@go-to-k/cdkd 0.251.0 → 0.252.1

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 ensureAssetStorage, A as WAFv2WebACLProvider, B as LockManager, Bt as LocalInvokeBuildError$1, C as findActionableSilentDrops, Ct as CFN_TEMPLATE_BODY_LIMIT, D as IntrinsicFunctionResolver, Dt as uploadCfnTemplate, E as isTerminationProtectionPropagationError, Et as findLargeInlineResources, F as assertRegionMatch, Ft as setAwsClients, G as stringifyValue, Gt as NestedStackChildDirectDestroyError, H as rebuildClientForBucketRegion, Ht as LocalStartServiceError, I as applyRoleArnIfSet, J as createAssetRedirectResolver, Jt as ResourceTimeoutError, K as WorkGraph, Kt as PartialFailureError, L as DiffCalculator, Lt as CdkdError, M as matchesCdkPath, Mt as AwsClients, N as normalizeAwsTagsToCfn, Nt as getAwsClients, O as cfnRefValueFromPhysicalId, Ot as expectedOwnerParam, P as resolveExplicitPhysicalId, Pt as resetAwsClients, Q as BOOTSTRAP_MARKER_PREFIX, R as DagBuilder, S as ProviderRegistry, St as warnDeprecatedNoPrefixCliFlag, T as disableInstanceApiTermination, Tt as MIGRATE_TMP_PREFIX, U as shouldRetainResource, V as S3StateBackend, Vt as LocalMigrateError, W as AssetPublisher, Wt as MissingCdkCliError, X as rewriteTemplateAssetReferences, Xt as StackHasActiveImportsError, Y as loadPublishableAssetManifest, Yt as ResourceUpdateNotSupportedError, Z as AssetModeResolver, Zt as StackTerminationProtectionError, _ as green, _t as resolveCaptureObservedState, a as withRetry, at as formatDockerLoginError, b as IAMRoleProvider, bt as resolveStateBucketWithDefaultAndSource, c as computeImplicitDeleteEdges, ct as runDockerStreaming, d as isStatefulRecreateTargetSync, dt as Synthesizer, et as getBootstrapMarkerKey, f as renderStatefulReason, ft as synthesisStatusMessage, g as gray, gt as resolveAutoAssetStorage, h as cyan, ht as resolveApp, i as withResourceDeadline, it as buildDockerImage, j as CDK_PATH_TAG, jt as resolveBucketRegion, k as refStateLookupFromResource, kt as AssemblyReader, l as extractDeploymentEventError, lt as AssetManifestLoader, m as bold, mt as getLegacyStateBucketName, n as DEFAULT_RESOURCE_WARN_AFTER_MS, nn as normalizeAwsError, nt as validateAssetBucketName, o as isRetryableTransientError, ot as getDockerCmd, p as formatResourceLine, pt as getDefaultStateBucketName, q as buildAssetRedirectMap, qt as ProvisioningError, r as DeployEngine, rn as withErrorHandling, rt as validateContainerRepoName, s as IMPLICIT_DELETE_DEPENDENCIES, st as runDockerForeground, t as DEFAULT_RESOURCE_TIMEOUT_MS, tt as parseBootstrapMarker, u as MULTI_REGION_RECREATE_BLOCKED_TYPES, ut as getDockerImageBySourceHash, v as red, vt as resolveSkipPrefix, w as CloudControlProvider, wt as CFN_TEMPLATE_URL_LIMIT, x as collectInlinePolicyNamesManagedBySiblings, xt as resolveUseCdkBootstrapAssets, y as yellow, yt as resolveStateBucketWithDefault, z as TemplateParser } from "./deploy-engine-D-gBbMiZ.js";
2
+ import { $ as BOOTSTRAP_MARKER_PREFIX, A as refStateLookupFromResource, At as AssemblyReader, B as TemplateParser, C as ProviderRegistry, Ct as warnDeprecatedNoPrefixCliFlag, D as isTerminationProtectionPropagationError, Dt as findLargeInlineResources, E as disableInstanceApiTermination, Et as MIGRATE_TMP_PREFIX, F as resolveExplicitPhysicalId, Ft as resetAwsClients, G as AssetPublisher, Gt as MissingCdkCliError, H as S3StateBackend, Ht as LocalMigrateError, I as assertRegionMatch, It as setAwsClients, J as buildAssetRedirectMap, Jt as ProvisioningError, K as stringifyValue, Kt as NestedStackChildDirectDestroyError, L as applyRoleArnIfSet, M as CDK_PATH_TAG, Mt as resolveBucketRegion, N as matchesCdkPath, Nt as AwsClients, O as IntrinsicFunctionResolver, Ot as uploadCfnTemplate, P as normalizeAwsTagsToCfn, Pt as getAwsClients, Q as AssetModeResolver, Qt as StackTerminationProtectionError, R as DiffCalculator, Rt as CdkdError, S as collectInlinePolicyNamesManagedBySiblings, St as resolveUseCdkBootstrapAssets, T as CloudControlProvider, Tt as CFN_TEMPLATE_URL_LIMIT, U as rebuildClientForBucketRegion, Ut as LocalStartServiceError, V as LockManager, Vt as LocalInvokeBuildError$1, W as shouldRetainResource, X as loadPublishableAssetManifest, Xt as ResourceUpdateNotSupportedError, Y as createAssetRedirectResolver, Yt as ResourceTimeoutError, Z as rewriteTemplateAssetReferences, Zt as StackHasActiveImportsError, _ as gray, _t as resolveAutoAssetStorage, a as withRetry, at as buildDockerImage, b as yellow, bt as resolveStateBucketWithDefault, c as IMPLICIT_DELETE_DEPENDENCIES, ct as runDockerForeground, d as MULTI_REGION_RECREATE_BLOCKED_TYPES, dt as getDockerImageBySourceHash, et as ensureAssetStorage, f as isStatefulRecreateTargetSync, ft as Synthesizer, g as cyan, gt as resolveApp, h as bold, ht as getLegacyStateBucketName, i as withResourceDeadline, in as withErrorHandling, it as validateContainerRepoName, j as WAFv2WebACLProvider, k as cfnRefValueFromPhysicalId, kt as expectedOwnerParam, l as computeImplicitDeleteEdges, lt as runDockerStreaming, m as formatResourceLine, mt as getDefaultStateBucketName, n as DEFAULT_RESOURCE_WARN_AFTER_MS, nt as parseBootstrapMarker, o as isRetryableTransientError, ot as formatDockerLoginError, p as renderStatefulReason, pt as synthesisStatusMessage, q as WorkGraph, qt as PartialFailureError, r as DeployEngine, rn as normalizeAwsError, rt as validateAssetBucketName, s as isThrottlingError, st as getDockerCmd, t as DEFAULT_RESOURCE_TIMEOUT_MS, tt as getBootstrapMarkerKey, u as extractDeploymentEventError, ut as AssetManifestLoader, v as green, vt as resolveCaptureObservedState, w as findActionableSilentDrops, wt as CFN_TEMPLATE_BODY_LIMIT, x as IAMRoleProvider, xt as resolveStateBucketWithDefaultAndSource, y as red, yt as resolveSkipPrefix, z as DagBuilder } from "./deploy-engine-IkhnJXWR.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-er3xGYRo.js";
4
+ import { t as ASGProvider } from "./asg-provider-BzBdoSYp.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, DeleteBucketIntelligentTieringConfigurationCommand, DeleteBucketInventoryConfigurationCommand, DeleteBucketLifecycleCommand, DeleteBucketMetricsConfigurationCommand, DeleteBucketPolicyCommand, DeleteBucketReplicationCommand, DeleteBucketTaggingCommand, DeleteBucketWebsiteCommand, DeleteObjectsCommand, GetBucketAccelerateConfigurationCommand, GetBucketCorsCommand, GetBucketEncryptionCommand, GetBucketLifecycleConfigurationCommand, GetBucketLocationCommand, GetBucketLoggingCommand, GetBucketNotificationConfigurationCommand, GetBucketPolicyCommand, GetBucketReplicationCommand, GetBucketTaggingCommand, GetBucketVersioningCommand, GetBucketWebsiteCommand, GetObjectCommand, GetObjectLockConfigurationCommand, GetPublicAccessBlockCommand, HeadBucketCommand, ListBucketAnalyticsConfigurationsCommand, ListBucketIntelligentTieringConfigurationsCommand, ListBucketInventoryConfigurationsCommand, ListBucketMetricsConfigurationsCommand, ListBucketsCommand, ListDirectoryBucketsCommand, 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";
@@ -1888,7 +1888,7 @@ const FLUSH_INTERVAL_MS = 2e3;
1888
1888
  const FLUSH_EVENT_THRESHOLD = 50;
1889
1889
  /** Build-time cdkd version, with a dev fallback for non-built contexts. */
1890
1890
  function getCdkdVersion() {
1891
- return "0.251.0";
1891
+ return "0.252.1";
1892
1892
  }
1893
1893
  /**
1894
1894
  * Generate a time-sortable unique run id, e.g.
@@ -25221,6 +25221,155 @@ var RDSDBProxyTargetGroupProvider = class {
25221
25221
  }
25222
25222
  };
25223
25223
 
25224
+ //#endregion
25225
+ //#region src/provisioning/import-tag-walk.ts
25226
+ /**
25227
+ * Shared, throttle-tolerant `aws:cdk:path` tag walk for `ResourceProvider.import`.
25228
+ *
25229
+ * Providers that adopt a resource without an explicit name property fall back
25230
+ * to step 3 of the lookup order documented in `./import-helpers.ts`: enumerate
25231
+ * the service's `List*`/`Describe*` pages, then issue ONE per-candidate read
25232
+ * (`DescribeX` / `ListTagsForResource`) to obtain the tag set — the list
25233
+ * summaries usually do not carry tags. That is an inherent **N+1** read
25234
+ * pattern: an account with many resources of the type produces one API call
25235
+ * per candidate in a tight loop, which is exactly the shape AWS rate-limits.
25236
+ *
25237
+ * Every provider previously hand-rolled this loop with NO backoff, so a single
25238
+ * throttled `Describe*` aborted the whole `cdkd import` run. This helper
25239
+ * centralises the loop and wraps BOTH the list page fetch and the per-candidate
25240
+ * describe in the deploy engine's `withRetry` with exponential backoff.
25241
+ *
25242
+ * ## Why a narrower classifier than `isRetryableTransientError`
25243
+ *
25244
+ * The deploy engine's classifier is tuned for the WRITE path: it also treats
25245
+ * eventual-consistency phrasings like `does not exist` / `not authorized to
25246
+ * perform` as transient, because a just-created dependency legitimately needs a
25247
+ * moment to propagate. On a read-only import walk those messages mean the
25248
+ * opposite — the candidate really is gone, or the caller's credentials really
25249
+ * lack the permission — and retrying them burns the full backoff budget per
25250
+ * candidate before surfacing the true error.
25251
+ *
25252
+ * So the walk retries throttling ONLY, reusing the deploy engine's throttle
25253
+ * tables verbatim ({@link THROTTLING_ERROR_NAMES} /
25254
+ * {@link RETRYABLE_HTTP_STATUS_CODES}) rather than maintaining a second copy.
25255
+ *
25256
+ * ## Batching
25257
+ *
25258
+ * Batched tag reads are deliberately NOT modelled here: the services this
25259
+ * helper currently serves (EMR `DescribeCluster`, DocDB
25260
+ * `ListTagsForResource`) expose only single-resource reads. A service that
25261
+ * genuinely offers a batch API (e.g. CodeCommit `BatchGetRepositories`) can
25262
+ * satisfy several candidates from one call inside its own `describe` callback,
25263
+ * or bypass the helper entirely.
25264
+ */
25265
+ /** Max number of retries after the first attempt, per API call in the walk. */
25266
+ const DEFAULT_MAX_RETRIES = 5;
25267
+ /** Initial backoff; each retry doubles it up to {@link DEFAULT_MAX_DELAY_MS}. */
25268
+ const DEFAULT_INITIAL_DELAY_MS = 500;
25269
+ /** Cap for the per-retry delay (0.5s -> 1s -> 2s -> 4s -> 5s, ~12.5s total). */
25270
+ const DEFAULT_MAX_DELAY_MS = 5e3;
25271
+ /**
25272
+ * Wall-clock ceiling for the WHOLE walk (all pages + all candidates), not just
25273
+ * one call. Backoff is per-call, so without this a sustained throttle against a
25274
+ * large account degrades into `(pages + candidates) x ~12.5s` of near-silent
25275
+ * retrying — ~42 minutes for 200 candidates, with no way to tell a slow walk
25276
+ * from a hung one. 10 minutes is generous for a healthy walk (a 200-candidate
25277
+ * DocDB account completes in seconds when AWS is not throttling) and bounds the
25278
+ * pathological case to something a user will wait through.
25279
+ */
25280
+ const DEFAULT_MAX_WALK_MS = 10 * 6e4;
25281
+ /**
25282
+ * Ceiling on pages fetched. A service that returns a non-advancing pagination
25283
+ * token (a bug, or a marker cdkd echoes back wrongly) would otherwise spin
25284
+ * forever. This is now the shared path every migrated provider runs on, so the
25285
+ * guard belongs here rather than in each caller.
25286
+ */
25287
+ const DEFAULT_MAX_PAGES = 1e3;
25288
+ /**
25289
+ * Whether an error hit during the read-only tag walk is a rate-limit rejection
25290
+ * worth backing off on.
25291
+ *
25292
+ * Delegates the error + `.cause` chain traversal to the deploy engine's
25293
+ * {@link isThrottlingError} (throttling error names + retryable HTTP statuses,
25294
+ * at every cause depth) and adds the canonical `Rate exceeded` message, which
25295
+ * several services return with an HTTP 400 and a service-specific error name.
25296
+ *
25297
+ * This is deliberately NARROWER than `isRetryableTransientError`: that
25298
+ * classifier also treats write-path eventual-consistency phrasings (`does not
25299
+ * exist`, `not authorized to perform`) as transient, because a just-created
25300
+ * dependency legitimately needs a moment to propagate. On a read-only walk
25301
+ * those mean the candidate really is gone or the credentials really lack the
25302
+ * permission, and retrying them burns the full backoff budget per candidate
25303
+ * before surfacing the true error.
25304
+ *
25305
+ * Exported for direct unit testing and for providers whose tag walk cannot use
25306
+ * {@link importTagWalk} verbatim.
25307
+ */
25308
+ function isThrottlingLikeError(error, message) {
25309
+ return isThrottlingError(error) || message.includes("Rate exceeded");
25310
+ }
25311
+ /** Thrown when the walk exceeds its wall-clock budget or page ceiling. */
25312
+ var ImportTagWalkLimitError = class extends Error {
25313
+ constructor(message) {
25314
+ super(message);
25315
+ this.name = "ImportTagWalkLimitError";
25316
+ }
25317
+ };
25318
+ /**
25319
+ * Paginate `listPage`, `describe` each candidate, and return the first one
25320
+ * whose tags carry the requested `aws:cdk:path`. Returns `null` when no
25321
+ * candidate matches (or when `cdkPath` is empty).
25322
+ *
25323
+ * Both callbacks are individually retried with exponential backoff on
25324
+ * throttling errors, so one rate-limited call mid-walk no longer aborts the
25325
+ * whole import.
25326
+ */
25327
+ async function importTagWalk(options) {
25328
+ const { cdkPath, listPage, describe, tagsOf } = options;
25329
+ if (!cdkPath) return null;
25330
+ const logicalId = options.logicalId ?? "import";
25331
+ const logger = options.retry?.logger ?? getLogger();
25332
+ const maxWalkMs = options.retry?.maxWalkMs ?? DEFAULT_MAX_WALK_MS;
25333
+ const maxPages = options.retry?.maxPages ?? DEFAULT_MAX_PAGES;
25334
+ const retryOpts = {
25335
+ maxRetries: options.retry?.maxRetries ?? DEFAULT_MAX_RETRIES,
25336
+ initialDelayMs: options.retry?.initialDelayMs ?? DEFAULT_INITIAL_DELAY_MS,
25337
+ maxDelayMs: options.retry?.maxDelayMs ?? DEFAULT_MAX_DELAY_MS,
25338
+ isRetryable: (message, error) => isThrottlingLikeError(error, message),
25339
+ logger,
25340
+ ...options.retry?.isInterrupted && { isInterrupted: options.retry.isInterrupted },
25341
+ ...options.retry?.onInterrupted && { onInterrupted: options.retry.onInterrupted },
25342
+ ...options.retry?.sleep && { sleep: options.retry.sleep }
25343
+ };
25344
+ const startedAt = Date.now();
25345
+ const assertWithinBudget = (stage) => {
25346
+ const elapsed = Date.now() - startedAt;
25347
+ if (elapsed >= maxWalkMs) throw new ImportTagWalkLimitError(`Timed out looking up ${logicalId} by its aws:cdk:path tag after ${Math.round(elapsed / 1e3)}s (limit ${Math.round(maxWalkMs / 1e3)}s, while ${stage}). AWS is likely throttling the lookup; retry, or pass an explicit physical id with --resource ${logicalId}=<physicalId>.`);
25348
+ };
25349
+ let marker;
25350
+ let pages = 0;
25351
+ do {
25352
+ assertWithinBudget("listing candidates");
25353
+ if (++pages > maxPages) throw new ImportTagWalkLimitError(`Gave up looking up ${logicalId} by its aws:cdk:path tag after ${maxPages} pages (the service may be returning a non-advancing pagination token). Pass an explicit physical id with --resource ${logicalId}=<physicalId>.`);
25354
+ const currentMarker = marker;
25355
+ const page = await withRetry(() => listPage(currentMarker), logicalId, retryOpts);
25356
+ for (const summary of page.items ?? []) {
25357
+ assertWithinBudget("reading candidate tags");
25358
+ const detail = await withRetry(() => describe(summary), logicalId, retryOpts);
25359
+ if (detail === void 0) {
25360
+ logger.debug(` ↷ Skipped an ${logicalId} import candidate: its detail lookup returned no result`);
25361
+ continue;
25362
+ }
25363
+ if (matchesCdkPath(tagsOf(detail, summary), cdkPath)) return {
25364
+ summary,
25365
+ detail
25366
+ };
25367
+ }
25368
+ marker = page.nextMarker;
25369
+ } while (marker);
25370
+ return null;
25371
+ }
25372
+
25224
25373
  //#endregion
25225
25374
  //#region src/provisioning/providers/docdb-provider.ts
25226
25375
  /**
@@ -25696,6 +25845,9 @@ var DocDBProvider = class {
25696
25845
  * Identifier` / `DBClusterIdentifier` / `DBSubnetGroupName`) are usually
25697
25846
  * present in CDK templates; fall back to `aws:cdk:path` tag lookup via
25698
25847
  * the corresponding `Describe*` + `ListTagsForResource` pair otherwise.
25848
+ * The fallback walk runs through the shared `importTagWalk` helper, so the
25849
+ * N+1 `ListTagsForResource` burst is retried with exponential backoff when
25850
+ * AWS throttles it instead of aborting the whole import.
25699
25851
  */
25700
25852
  async import(input) {
25701
25853
  switch (input.resourceType) {
@@ -25811,20 +25963,24 @@ var DocDBProvider = class {
25811
25963
  if (err.name === "DBInstanceNotFoundFault") return null;
25812
25964
  throw err;
25813
25965
  }
25814
- if (!input.cdkPath) return null;
25815
- let marker;
25816
- do {
25817
- const list = await this.getClient().send(new DescribeDBInstancesCommand$1({ ...marker && { Marker: marker } }));
25818
- for (const inst of list.DBInstances ?? []) {
25819
- if (!inst.DBInstanceIdentifier || !inst.DBInstanceArn) continue;
25820
- if (matchesCdkPath((await this.getClient().send(new ListTagsForResourceCommand$13({ ResourceName: inst.DBInstanceArn }))).TagList, input.cdkPath)) return {
25821
- physicalId: inst.DBInstanceIdentifier,
25822
- attributes: {}
25966
+ const match = await importTagWalk({
25967
+ cdkPath: input.cdkPath,
25968
+ logicalId: input.logicalId,
25969
+ listPage: async (marker) => {
25970
+ const list = await this.getClient().send(new DescribeDBInstancesCommand$1({ ...marker && { Marker: marker } }));
25971
+ return {
25972
+ items: list.DBInstances,
25973
+ nextMarker: list.Marker
25823
25974
  };
25824
- }
25825
- marker = list.Marker;
25826
- } while (marker);
25827
- return null;
25975
+ },
25976
+ describe: async (inst) => inst.DBInstanceIdentifier && inst.DBInstanceArn ? await this.listTagsForResource(inst.DBInstanceArn) : void 0,
25977
+ tagsOf: (tags) => tags
25978
+ });
25979
+ if (!match) return null;
25980
+ return {
25981
+ physicalId: match.summary.DBInstanceIdentifier,
25982
+ attributes: {}
25983
+ };
25828
25984
  }
25829
25985
  async importDBCluster(input) {
25830
25986
  const explicit = resolveExplicitPhysicalId(input, "DBClusterIdentifier");
@@ -25838,20 +25994,24 @@ var DocDBProvider = class {
25838
25994
  if (err.name === "DBClusterNotFoundFault") return null;
25839
25995
  throw err;
25840
25996
  }
25841
- if (!input.cdkPath) return null;
25842
- let marker;
25843
- do {
25844
- const list = await this.getClient().send(new DescribeDBClustersCommand$1({ ...marker && { Marker: marker } }));
25845
- for (const c of list.DBClusters ?? []) {
25846
- if (!c.DBClusterIdentifier || !c.DBClusterArn) continue;
25847
- if (matchesCdkPath((await this.getClient().send(new ListTagsForResourceCommand$13({ ResourceName: c.DBClusterArn }))).TagList, input.cdkPath)) return {
25848
- physicalId: c.DBClusterIdentifier,
25849
- attributes: {}
25997
+ const match = await importTagWalk({
25998
+ cdkPath: input.cdkPath,
25999
+ logicalId: input.logicalId,
26000
+ listPage: async (marker) => {
26001
+ const list = await this.getClient().send(new DescribeDBClustersCommand$1({ ...marker && { Marker: marker } }));
26002
+ return {
26003
+ items: list.DBClusters,
26004
+ nextMarker: list.Marker
25850
26005
  };
25851
- }
25852
- marker = list.Marker;
25853
- } while (marker);
25854
- return null;
26006
+ },
26007
+ describe: async (c) => c.DBClusterIdentifier && c.DBClusterArn ? await this.listTagsForResource(c.DBClusterArn) : void 0,
26008
+ tagsOf: (tags) => tags
26009
+ });
26010
+ if (!match) return null;
26011
+ return {
26012
+ physicalId: match.summary.DBClusterIdentifier,
26013
+ attributes: {}
26014
+ };
25855
26015
  }
25856
26016
  async importDBSubnetGroup(input) {
25857
26017
  const explicit = resolveExplicitPhysicalId(input, "DBSubnetGroupName");
@@ -25865,20 +26025,33 @@ var DocDBProvider = class {
25865
26025
  if (err.name === "DBSubnetGroupNotFoundFault") return null;
25866
26026
  throw err;
25867
26027
  }
25868
- if (!input.cdkPath) return null;
25869
- let marker;
25870
- do {
25871
- const list = await this.getClient().send(new DescribeDBSubnetGroupsCommand$1({ ...marker && { Marker: marker } }));
25872
- for (const sg of list.DBSubnetGroups ?? []) {
25873
- if (!sg.DBSubnetGroupName || !sg.DBSubnetGroupArn) continue;
25874
- if (matchesCdkPath((await this.getClient().send(new ListTagsForResourceCommand$13({ ResourceName: sg.DBSubnetGroupArn }))).TagList, input.cdkPath)) return {
25875
- physicalId: sg.DBSubnetGroupName,
25876
- attributes: {}
26028
+ const match = await importTagWalk({
26029
+ cdkPath: input.cdkPath,
26030
+ logicalId: input.logicalId,
26031
+ listPage: async (marker) => {
26032
+ const list = await this.getClient().send(new DescribeDBSubnetGroupsCommand$1({ ...marker && { Marker: marker } }));
26033
+ return {
26034
+ items: list.DBSubnetGroups,
26035
+ nextMarker: list.Marker
25877
26036
  };
25878
- }
25879
- marker = list.Marker;
25880
- } while (marker);
25881
- return null;
26037
+ },
26038
+ describe: async (sg) => sg.DBSubnetGroupName && sg.DBSubnetGroupArn ? await this.listTagsForResource(sg.DBSubnetGroupArn) : void 0,
26039
+ tagsOf: (tags) => tags
26040
+ });
26041
+ if (!match) return null;
26042
+ return {
26043
+ physicalId: match.summary.DBSubnetGroupName,
26044
+ attributes: {}
26045
+ };
26046
+ }
26047
+ /**
26048
+ * `ListTagsForResource` for the tag-walk import path. Returns the tag list
26049
+ * (possibly empty) so `importTagWalk` can match `aws:cdk:path` against it;
26050
+ * `undefined` is reserved for "skip this candidate", which the callers signal
26051
+ * before calling this.
26052
+ */
26053
+ async listTagsForResource(resourceArn) {
26054
+ return (await this.getClient().send(new ListTagsForResourceCommand$13({ ResourceName: resourceArn }))).TagList ?? [];
25882
26055
  }
25883
26056
  };
25884
26057
 
@@ -35382,6 +35555,38 @@ var FSxFileSystemProvider = class {
35382
35555
  ];
35383
35556
  }
35384
35557
  /**
35558
+ * Plain-string arrays FSx returns as unordered sets.
35559
+ *
35560
+ * - `WindowsConfiguration.Aliases` — DNS alias names (`files.example.com`).
35561
+ * Alternate names the file system answers to; the API documents no
35562
+ * ordering semantics and no name is privileged over another, so a
35563
+ * `DescribeFileSystems` reorder carries no meaning.
35564
+ *
35565
+ * This does not match the shared normalizer's AWS-id / ARN heuristic, so
35566
+ * without the declaration a reorder would surface as phantom drift. Declared
35567
+ * here rather than sorted in `readWindowsConfiguration` so the sort applies
35568
+ * to BOTH comparison sides — see
35569
+ * {@link ResourceProvider.getDriftUnorderedPaths}.
35570
+ *
35571
+ * Deliberately NOT declared:
35572
+ *
35573
+ * - `WindowsConfiguration.SelfManagedActiveDirectoryConfiguration.DnsIps` —
35574
+ * the API reference describes it only as "A list of IP addresses of DNS
35575
+ * servers or domain controllers in the self-managed AD directory"
35576
+ * (https://docs.aws.amazon.com/fsx/latest/APIReference/API_SelfManagedActiveDirectoryConfiguration.html),
35577
+ * with no statement that order is insignificant. DNS resolver lists are
35578
+ * conventionally preference-ordered (primary first), and if FSx honors
35579
+ * that when joining the domain, sorting would HIDE a real reorder. A
35580
+ * false positive is visible and correctable; silently hiding drift is
35581
+ * not. Same reasoning excludes ElastiCache `PreferredAvailabilityZones`.
35582
+ * - `OntapConfiguration.RouteTableIds` / `OpenZFSConfiguration.RouteTableIds`
35583
+ * — their `rtb-` elements already match the shared id heuristic.
35584
+ */
35585
+ getDriftUnorderedPaths(resourceType) {
35586
+ if (resourceType !== "AWS::FSx::FileSystem") return [];
35587
+ return ["WindowsConfiguration.Aliases"];
35588
+ }
35589
+ /**
35385
35590
  * Read the AWS-current file system configuration in CFn-property shape
35386
35591
  * via `DescribeFileSystems`. Lustre data-repository fields (`ImportPath`
35387
35592
  * / `ExportPath` / `AutoImportPolicy` / `ImportedFileChunkSize`) are
@@ -36037,45 +36242,58 @@ var EMRClusterProvider = class {
36037
36242
  * 2. Tag-based lookup: `ListClusters` filtered to the non-terminated states
36038
36243
  * (a `TERMINATED*` cluster is gone and must never be adopted), then a
36039
36244
  * `DescribeCluster` per candidate to read `Tags` (the list summaries do
36040
- * NOT carry tags) and match `aws:cdk:path`.
36245
+ * NOT carry tags) and match `aws:cdk:path`. The walk runs through the
36246
+ * shared `importTagWalk` helper, so each list page / describe is retried
36247
+ * with exponential backoff when AWS throttles the N+1 read burst.
36041
36248
  */
36042
36249
  async import(input) {
36043
36250
  const explicit = resolveExplicitPhysicalId(input, null);
36044
36251
  if (explicit) {
36045
- const cluster = await this.describeClusterOrNull(explicit);
36252
+ const cluster = await this.describeClusterOrUndefined(explicit);
36046
36253
  if (!cluster || cluster.Status?.State && TERMINAL_STATES.has(cluster.Status.State)) return null;
36047
36254
  return {
36048
36255
  physicalId: explicit,
36049
36256
  attributes: this.buildAttributes(cluster)
36050
36257
  };
36051
36258
  }
36052
- if (!input.cdkPath) return null;
36053
- let marker;
36054
- do {
36055
- const list = await this.getClient().send(new ListClustersCommand$1({
36056
- ClusterStates: [...NON_TERMINATED_STATES],
36057
- ...marker && { Marker: marker }
36058
- }));
36059
- for (const summary of list.Clusters ?? []) {
36060
- if (!summary.Id) continue;
36061
- const cluster = await this.describeClusterOrNull(summary.Id);
36062
- if (!cluster) continue;
36063
- if (matchesCdkPath(cluster.Tags, input.cdkPath)) return {
36064
- physicalId: summary.Id,
36065
- attributes: this.buildAttributes(cluster)
36259
+ const match = await importTagWalk({
36260
+ cdkPath: input.cdkPath,
36261
+ logicalId: input.logicalId,
36262
+ listPage: async (marker) => {
36263
+ const list = await this.getClient().send(new ListClustersCommand$1({
36264
+ ClusterStates: [...NON_TERMINATED_STATES],
36265
+ ...marker && { Marker: marker }
36266
+ }));
36267
+ return {
36268
+ items: list.Clusters,
36269
+ nextMarker: list.Marker
36066
36270
  };
36067
- }
36068
- marker = list.Marker;
36069
- } while (marker);
36070
- return null;
36271
+ },
36272
+ describe: async (summary) => summary.Id ? await this.describeClusterOrUndefined(summary.Id) : void 0,
36273
+ tagsOf: (cluster) => cluster.Tags
36274
+ });
36275
+ if (!match) return null;
36276
+ return {
36277
+ physicalId: match.summary.Id,
36278
+ attributes: this.buildAttributes(match.detail)
36279
+ };
36071
36280
  }
36072
36281
  /**
36073
36282
  * `DescribeCluster` that maps a not-found (`InvalidRequestException` — the
36074
- * cluster id is unknown in this region / aged out of Describe) to `null`
36075
- * instead of throwing, so `import()` can treat it as "no match" rather than
36076
- * aborting the whole adoption run.
36283
+ * cluster id is unknown in this region / aged out of Describe) to
36284
+ * `undefined` instead of throwing, so `import()` can treat it as "no match"
36285
+ * rather than aborting the whole adoption run.
36286
+ *
36287
+ * `undefined`, NOT `null`, is load-bearing: `importTagWalk` skips a candidate
36288
+ * on `detail === undefined`, and an actual `null` would flow into `tagsOf`
36289
+ * and `TypeError`. Also note the mapping is BROAD — `InvalidRequestException`
36290
+ * covers more than not-found, so a genuine failure here degrades to "no
36291
+ * match" and a subsequent deploy would CREATE a duplicate cluster rather than
36292
+ * adopt the existing one. The walk logs each skip at debug so the case is at
36293
+ * least visible under `--verbose`; narrowing the mapping needs a
36294
+ * message/code-level discriminator AWS does not currently document.
36077
36295
  */
36078
- async describeClusterOrNull(clusterId) {
36296
+ async describeClusterOrUndefined(clusterId) {
36079
36297
  try {
36080
36298
  return (await this.getClient().send(new DescribeClusterCommand({ ClusterId: clusterId }))).Cluster;
36081
36299
  } catch (err) {
@@ -36114,7 +36332,7 @@ var EMRClusterProvider = class {
36114
36332
  * optional `import` method's incremental opt-in shape.
36115
36333
  */
36116
36334
  async readCurrentState(physicalId, _logicalId, _resourceType) {
36117
- const cluster = await this.describeClusterOrNull(physicalId);
36335
+ const cluster = await this.describeClusterOrUndefined(physicalId);
36118
36336
  if (!cluster) return void 0;
36119
36337
  let instanceGroups = [];
36120
36338
  let instanceFleets = [];
@@ -36314,14 +36532,21 @@ const DEFAULT_POLL_INTERVAL_MS$1 = 15e3;
36314
36532
  /** Instance-group state that means "the group is up and idle/serving". */
36315
36533
  const READY_STATES$1 = /* @__PURE__ */ new Set(["RUNNING"]);
36316
36534
  /**
36317
- * Instance-group states that mean "the group is gone / failed" — a hard
36318
- * error during a create/resize wait (an ARRESTED group failed to provision;
36319
- * TERMINATED/ENDED mean it will never reach RUNNING).
36535
+ * Instance-group states that mean "the group will never reach the requested
36536
+ * size" — a hard error during a create/resize wait.
36537
+ *
36538
+ * `ARRESTED` means the group failed to provision; `TERMINATED`/`ENDED` mean it
36539
+ * is gone. `SUSPENDED` means a resize could not complete: the existing
36540
+ * instances keep running but AWS can no longer add or remove any, so the wait
36541
+ * would poll to the full `maxWaitMs` timeout instead of failing fast with the
36542
+ * service's own state-change reason — the exact defect fixed on the
36543
+ * instance-fleet side in issue #1092 item 2, which applies verbatim here.
36320
36544
  */
36321
36545
  const FAILED_STATES$1 = /* @__PURE__ */ new Set([
36322
36546
  "ARRESTED",
36323
- "TERMINATED",
36324
- "ENDED"
36547
+ "ENDED",
36548
+ "SUSPENDED",
36549
+ "TERMINATED"
36325
36550
  ]);
36326
36551
  const toNumber$1 = (v) => {
36327
36552
  if (v === void 0) return void 0;
@@ -36471,7 +36696,7 @@ var EMRInstanceGroupConfigProvider = class {
36471
36696
  InstanceCount: toNumber$1(properties["InstanceCount"])
36472
36697
  }]
36473
36698
  }));
36474
- if (jobFlowId) await this.waitForGroupReady(jobFlowId, physicalId, logicalId, resourceType, toNumber$1(properties["InstanceCount"]) ?? 0);
36699
+ if (jobFlowId) await this.waitForGroupReady(jobFlowId, physicalId, logicalId, resourceType, toNumber$1(properties["InstanceCount"]) ?? 0, true);
36475
36700
  }
36476
36701
  if (autoScalingChanged) {
36477
36702
  if (!jobFlowId) throw new ProvisioningError(`EMR InstanceGroupConfig ${logicalId} AutoScalingPolicy update needs JobFlowId (the cluster id), which is absent`, resourceType, logicalId, physicalId);
@@ -36532,15 +36757,43 @@ var EMRInstanceGroupConfigProvider = class {
36532
36757
  * would return before the resize even starts. Waiting on the running count
36533
36758
  * reaching the requested target settles both create (0 -> N provisioning)
36534
36759
  * and resize (M -> N add/remove) correctly.
36535
- */
36536
- async waitForGroupReady(clusterId, groupId, logicalId, resourceType, expectedCount) {
36760
+ *
36761
+ * `toleratesStaleFailedState` MUST be true on the resize path and false on
36762
+ * create. It exists for the RECOVERY case: `SUSPENDED` (a resize that could
36763
+ * not complete) is precisely the state a user re-runs `cdkd deploy` to fix,
36764
+ * but the same pre-resize read lag documented above means the first poll
36765
+ * after `ModifyInstanceGroups` still reports `SUSPENDED`. Enforcing
36766
+ * {@link FAILED_STATES} on that stale read would abort the very resize that
36767
+ * recovers the group, on every attempt, making the state permanently
36768
+ * unrecoverable through cdkd.
36769
+ *
36770
+ * So on a resize the failed-state check is suppressed until the group is
36771
+ * observed LEAVING whatever state it started in; from then on it is enforced
36772
+ * normally (a resize that transitions RUNNING -> RESIZING -> SUSPENDED still
36773
+ * fails fast, which is the case #1092 item 2 is about). A group that never
36774
+ * leaves its initial state falls through to the `maxWaitMs` timeout — the
36775
+ * pre-fix behavior, and strictly better than an unrecoverable resource.
36776
+ *
36777
+ * Note the asymmetry that makes this specific to failed states: a stale
36778
+ * pre-resize `RUNNING` is harmless because the instance-count mismatch keeps
36779
+ * the loop polling. A stale `SUSPENDED` is terminal.
36780
+ */
36781
+ async waitForGroupReady(clusterId, groupId, logicalId, resourceType, expectedCount, toleratesStaleFailedState = false) {
36537
36782
  const startTime = Date.now();
36538
36783
  const transientState = { count: 0 };
36784
+ let initialState;
36785
+ let hasLeftInitialState = false;
36539
36786
  while (Date.now() - startTime < this.maxWaitMs) {
36540
36787
  const group = await this.findGroupForPoll(clusterId, groupId, transientState);
36541
36788
  const state = group?.Status?.State;
36789
+ if (state) {
36790
+ if (initialState === void 0) initialState = state;
36791
+ else if (state !== initialState) hasLeftInitialState = true;
36792
+ }
36542
36793
  if (state && READY_STATES$1.has(state) && group?.RunningInstanceCount === expectedCount) return;
36543
- if (state && FAILED_STATES$1.has(state)) throw new ProvisioningError(`EMR instance group ${groupId} entered failed state ${state}: ${group?.Status?.StateChangeReason?.Message ?? "no state-change reason reported"}`, resourceType, logicalId, groupId);
36794
+ const enforceFailedStates = !toleratesStaleFailedState || hasLeftInitialState;
36795
+ if (state && enforceFailedStates && FAILED_STATES$1.has(state)) throw new ProvisioningError(`EMR instance group ${groupId} entered failed state ${state}: ${group?.Status?.StateChangeReason?.Message ?? "no state-change reason reported"}`, resourceType, logicalId, groupId);
36796
+ if (state && !enforceFailedStates && FAILED_STATES$1.has(state)) this.logger.debug(`EMR instance group ${groupId} still reads ${state} from before the resize; waiting for it to leave that state before treating it as failed`);
36544
36797
  this.logger.debug(`EMR instance group ${groupId} state: ${state ?? "unknown"}, running ${group?.RunningInstanceCount ?? "?"}/${expectedCount}, waiting...`);
36545
36798
  await new Promise((resolve) => setTimeout(resolve, this.pollIntervalMs));
36546
36799
  }
@@ -36596,10 +36849,20 @@ const DEFAULT_POLL_INTERVAL_MS = 15e3;
36596
36849
  /** Instance-fleet state that means "the fleet is up and idle/serving". */
36597
36850
  const READY_STATES = /* @__PURE__ */ new Set(["RUNNING"]);
36598
36851
  /**
36599
- * Instance-fleet states that mean "the fleet is gone" a hard error during a
36600
- * create/resize wait (it will never reach RUNNING).
36852
+ * Instance-fleet states that mean "the fleet will never reach the requested
36853
+ * capacity" a hard error during a create/resize wait.
36854
+ *
36855
+ * `TERMINATED` means the fleet is gone. `SUSPENDED` means a resize could not
36856
+ * complete: the existing instances keep running but AWS can no longer add or
36857
+ * remove any, so the wait would poll to the full `maxWaitMs` timeout instead of
36858
+ * failing fast with the service's own state-change reason (issue #1092 item 2).
36859
+ *
36860
+ * This is the fleet-state ANALOGUE of the instance-group provider's failed set,
36861
+ * not a copy of it: the two enums differ. `ARRESTED` and `ENDED` are
36862
+ * `InstanceGroupState` members that do not exist for fleets, so `TERMINATED` is
36863
+ * the only state the two sets share.
36601
36864
  */
36602
- const FAILED_STATES = /* @__PURE__ */ new Set(["TERMINATED"]);
36865
+ const FAILED_STATES = /* @__PURE__ */ new Set(["SUSPENDED", "TERMINATED"]);
36603
36866
  const toNumber = (v) => {
36604
36867
  if (v === void 0) return void 0;
36605
36868
  const n = Number(v);
@@ -36752,7 +37015,7 @@ var EMRInstanceFleetConfigProvider = class {
36752
37015
  }));
36753
37016
  const newTarget = this.targetCapacity(properties);
36754
37017
  const prevTarget = this.targetCapacity(previousProperties);
36755
- await this.waitForFleetReady(clusterId, physicalId, logicalId, resourceType, newTarget, newTarget >= prevTarget);
37018
+ await this.waitForFleetReady(clusterId, physicalId, logicalId, resourceType, newTarget, newTarget >= prevTarget, true);
36756
37019
  this.logger.debug(`Successfully updated EMR instance fleet ${logicalId}`);
36757
37020
  return {
36758
37021
  physicalId,
@@ -36812,17 +37075,45 @@ var EMRInstanceFleetConfigProvider = class {
36812
37075
  *
36813
37076
  * A `targetCapacity` of 0 on the up path (never valid for a real create —
36814
37077
  * at least one target must be > 0) degrades to a State-only wait.
36815
- */
36816
- async waitForFleetReady(clusterId, fleetId, logicalId, resourceType, targetCapacity, atLeast) {
37078
+ *
37079
+ * `toleratesStaleFailedState` MUST be true on the resize path and false on
37080
+ * create. It exists for the RECOVERY case: `SUSPENDED` (a resize that could
37081
+ * not complete) is precisely the state a user re-runs `cdkd deploy` to fix,
37082
+ * but the same pre-resize read lag documented above means the first poll
37083
+ * after `ModifyInstanceFleet` still reports `SUSPENDED`. Enforcing
37084
+ * {@link FAILED_STATES} on that stale read would abort the very resize that
37085
+ * recovers the fleet, on every attempt, making the state permanently
37086
+ * unrecoverable through cdkd.
37087
+ *
37088
+ * So on a resize the failed-state check is suppressed until the fleet is
37089
+ * observed LEAVING whatever state it started in; from then on it is enforced
37090
+ * normally (a resize that transitions RUNNING -> RESIZING -> SUSPENDED still
37091
+ * fails fast, which is the case #1092 item 2 is about). A fleet that never
37092
+ * leaves its initial state falls through to the `maxWaitMs` timeout — the
37093
+ * pre-fix behavior, and strictly better than an unrecoverable resource.
37094
+ *
37095
+ * Note the asymmetry that makes this specific to failed states: a stale
37096
+ * pre-resize `RUNNING` is harmless because the capacity check keeps the loop
37097
+ * polling. A stale `SUSPENDED` is terminal.
37098
+ */
37099
+ async waitForFleetReady(clusterId, fleetId, logicalId, resourceType, targetCapacity, atLeast, toleratesStaleFailedState = false) {
36817
37100
  const startTime = Date.now();
36818
37101
  const transientState = { count: 0 };
37102
+ let initialState;
37103
+ let hasLeftInitialState = false;
36819
37104
  while (Date.now() - startTime < this.maxWaitMs) {
36820
37105
  const fleet = await this.findFleetForPoll(clusterId, fleetId, transientState);
36821
37106
  const state = fleet?.Status?.State;
36822
37107
  const provisioned = (fleet?.ProvisionedOnDemandCapacity ?? 0) + (fleet?.ProvisionedSpotCapacity ?? 0);
36823
37108
  const capacityReady = atLeast ? provisioned >= targetCapacity : provisioned <= targetCapacity;
37109
+ if (state) {
37110
+ if (initialState === void 0) initialState = state;
37111
+ else if (state !== initialState) hasLeftInitialState = true;
37112
+ }
36824
37113
  if (state && READY_STATES.has(state) && capacityReady) return;
36825
- if (state && FAILED_STATES.has(state)) throw new ProvisioningError(`EMR instance fleet ${fleetId} entered failed state ${state}: ${fleet?.Status?.StateChangeReason?.Message ?? "no state-change reason reported"}`, resourceType, logicalId, fleetId);
37114
+ const enforceFailedStates = !toleratesStaleFailedState || hasLeftInitialState;
37115
+ if (state && enforceFailedStates && FAILED_STATES.has(state)) throw new ProvisioningError(`EMR instance fleet ${fleetId} entered failed state ${state}: ${fleet?.Status?.StateChangeReason?.Message ?? "no state-change reason reported"}`, resourceType, logicalId, fleetId);
37116
+ if (state && !enforceFailedStates && FAILED_STATES.has(state)) this.logger.debug(`EMR instance fleet ${fleetId} still reads ${state} from before the resize; waiting for it to leave that state before treating it as failed`);
36826
37117
  this.logger.debug(`EMR instance fleet ${fleetId} state: ${state ?? "unknown"}, provisioned ${provisioned}/${targetCapacity}, waiting...`);
36827
37118
  await new Promise((resolve) => setTimeout(resolve, this.pollIntervalMs));
36828
37119
  }
@@ -44432,6 +44723,19 @@ function createDiffCommand() {
44432
44723
  * on BOTH sides before the diff. These two passes were surfaced by dogfooding
44433
44724
  * the sibling cdk-real-drift (cdkrd) tool, which hit the same false-positive
44434
44725
  * classes against the same kind of AWS-snapshot baseline.
44726
+ *
44727
+ * Plain-string arrays are NOT canonicalized by the two heuristic passes above,
44728
+ * because a scalar list can be order-significant. But several CFn inputs are
44729
+ * semantically unordered sets of plain strings (FSx `WindowsConfiguration.Aliases`,
44730
+ * `...SelfManagedActiveDirectoryConfiguration.DnsIps`, ...), so
44731
+ * {@link canonicalizeUnorderedArraysAtPaths} sorts them at an explicit,
44732
+ * provider-declared path list only — an opt-in seam mirroring
44733
+ * `getDriftUnknownPaths` (see `ResourceProvider.getDriftUnorderedPaths`).
44734
+ * Doing it HERE rather than inside the provider's reverse-mapper is load-bearing:
44735
+ * the normalizer runs on BOTH comparison sides, so it stays correct for the
44736
+ * `properties`-fallback baseline (a resource deployed before observed-capture,
44737
+ * whose baseline is the user's TEMPLATE order). Sorting only the AWS read side
44738
+ * would manufacture drift on exactly that path.
44435
44739
  */
44436
44740
  function canonicalizeTagListsDeep(v) {
44437
44741
  if (Array.isArray(v)) {
@@ -44466,6 +44770,72 @@ function canonicalizeIdArraysDeep(v) {
44466
44770
  }
44467
44771
  return v;
44468
44772
  }
44773
+ /**
44774
+ * Match a dotted property path against a provider-declared path list.
44775
+ *
44776
+ * An entry matches when the path is exactly equal to it, OR when the entry is
44777
+ * a prefix followed by `.`. Every entry is therefore a SUBTREE declaration:
44778
+ * `'VpcConfig'` matches `VpcConfig`, `VpcConfig.SubnetIds`, and anything
44779
+ * deeper. There is no leaf-only form.
44780
+ *
44781
+ * Shared by the two provider-declared path lists so their semantics cannot
44782
+ * drift apart: `getDriftUnknownPaths` (via `isIgnoredPath` in
44783
+ * `drift-calculator.ts`) and `getDriftUnorderedPaths` (via
44784
+ * {@link canonicalizeUnorderedArraysAtPaths} below).
44785
+ */
44786
+ function matchesPathPrefix(path, entries) {
44787
+ for (const entry of entries) {
44788
+ if (path === entry) return true;
44789
+ if (path.startsWith(`${entry}.`)) return true;
44790
+ }
44791
+ return false;
44792
+ }
44793
+ /**
44794
+ * Sort plain-string arrays found at one of the provider-declared `paths`, and
44795
+ * ONLY there. Unlike the two heuristic passes above, this one is opt-in per
44796
+ * resource type via `ResourceProvider.getDriftUnorderedPaths` — a scalar list
44797
+ * is order-significant unless the provider says otherwise.
44798
+ *
44799
+ * Paths use dot-notation for nested keys and are matched by the shared
44800
+ * {@link matchesPathPrefix} rule, the same one `getDriftUnknownPaths` uses. So
44801
+ * `'WindowsConfiguration'` covers every plain-string array in that subtree, and
44802
+ * `'WindowsConfiguration.Aliases'` covers that path and everything beneath it
44803
+ * (every entry is a subtree declaration — there is no leaf-only form).
44804
+ *
44805
+ * One semantic DIVERGENCE from `getDriftUnknownPaths`, required for this pass
44806
+ * to work: `isIgnoredPath` never sees a path that crosses an array, because the
44807
+ * comparator's `diffAt` compares arrays wholesale via `deepEqual` and never
44808
+ * descends into elements. This walk DOES descend into array elements, giving
44809
+ * each element its parent's path (array indices never appear in paths). So
44810
+ * `'Items.Aliases'` is meaningful here — it reaches an `Aliases` array inside
44811
+ * each element of `Items` — while being inert as an ignore-path. The divergence
44812
+ * is strictly more permissive; nothing that matches for ignore-paths fails to
44813
+ * match here.
44814
+ *
44815
+ * Only arrays whose every element is a plain string are sorted, so a
44816
+ * mis-declared path can never reorder object or mixed-type elements. Nested
44817
+ * arrays are also left alone: an array element that is itself an array is not
44818
+ * descended into, so `{P: [['b','a']]}` never has its INNER list sorted by a
44819
+ * `'P'` declaration (no CFn shape in tree is an array-of-arrays, and sorting
44820
+ * one would contradict the plain-string-elements-only rule).
44821
+ */
44822
+ function canonicalizeUnorderedArraysAtPaths(v, paths) {
44823
+ if (paths.length === 0) return v;
44824
+ return walkUnordered(v, "", paths);
44825
+ }
44826
+ function walkUnordered(v, path, paths) {
44827
+ if (Array.isArray(v)) {
44828
+ const mapped = v.map((el) => Array.isArray(el) ? el : walkUnordered(el, path, paths));
44829
+ if (mapped.length > 1 && matchesPathPrefix(path, paths) && mapped.every((el) => typeof el === "string")) return [...mapped].sort((a, b) => a < b ? -1 : a > b ? 1 : 0);
44830
+ return mapped;
44831
+ }
44832
+ if (v && typeof v === "object") {
44833
+ const out = {};
44834
+ for (const [k, val] of Object.entries(v)) out[k] = walkUnordered(val, path === "" ? k : `${path}.${k}`, paths);
44835
+ return out;
44836
+ }
44837
+ return v;
44838
+ }
44469
44839
 
44470
44840
  //#endregion
44471
44841
  //#region src/analyzer/drift-calculator.ts
@@ -44525,25 +44895,35 @@ function canonicalizeIdArraysDeep(v) {
44525
44895
  * it is exactly equal to the entry, or when the entry is a prefix followed
44526
44896
  * by `.` — so `'Code'` excludes the whole `Code` subtree, and
44527
44897
  * `'VpcConfig.SubnetIds'` excludes only that leaf.
44898
+ *
44899
+ * `options.unorderedPaths` is supplied by the provider (via
44900
+ * `getDriftUnorderedPaths`) for plain-string arrays that are semantically
44901
+ * UNORDERED sets — e.g. FSx `WindowsConfiguration.Aliases`. Those are sorted on
44902
+ * BOTH sides before comparison so an AWS-side reorder is not phantom drift.
44903
+ * Path matching uses the same prefix rule as `ignorePaths`.
44528
44904
  */
44529
44905
  function calculateResourceDrift(stateProperties, awsProperties, options) {
44530
44906
  const drifts = [];
44531
44907
  const ignore = options?.ignorePaths ?? [];
44532
44908
  const union = options?.unionWalkObjects ?? false;
44533
- stateProperties = canonicalizeIdArraysDeep(canonicalizeTagListsDeep(stateProperties));
44534
- awsProperties = canonicalizeIdArraysDeep(canonicalizeTagListsDeep(awsProperties));
44909
+ const unordered = options?.unorderedPaths ?? [];
44910
+ stateProperties = canonicalizeUnorderedArraysAtPaths(canonicalizeIdArraysDeep(canonicalizeTagListsDeep(stateProperties)), unordered);
44911
+ awsProperties = canonicalizeUnorderedArraysAtPaths(canonicalizeIdArraysDeep(canonicalizeTagListsDeep(awsProperties)), unordered);
44535
44912
  for (const key of Object.keys(stateProperties)) {
44536
44913
  if (isIgnoredPath(key, ignore)) continue;
44537
44914
  diffAt(key, stateProperties[key], awsProperties[key], drifts, ignore, union);
44538
44915
  }
44539
44916
  return drifts;
44540
44917
  }
44918
+ /**
44919
+ * Thin alias over the shared {@link matchesPathPrefix} rule, kept as a named
44920
+ * function because "ignored" is what the path list means at these call sites.
44921
+ * Sharing the implementation with `getDriftUnorderedPaths` is deliberate: both
44922
+ * lists are provider-declared and documented as reading the same way, so they
44923
+ * must not be able to drift apart.
44924
+ */
44541
44925
  function isIgnoredPath(path, ignorePaths) {
44542
- for (const entry of ignorePaths) {
44543
- if (path === entry) return true;
44544
- if (path.startsWith(`${entry}.`)) return true;
44545
- }
44546
- return false;
44926
+ return matchesPathPrefix(path, ignorePaths);
44547
44927
  }
44548
44928
  /**
44549
44929
  * Recursive worker. Pushes drift entries into `out` rather than
@@ -44945,10 +45325,12 @@ async function runDriftForStack(stackName, region, stateBackend, providerRegistr
44945
45325
  continue;
44946
45326
  }
44947
45327
  const ignorePaths = provider.getDriftUnknownPaths ? provider.getDriftUnknownPaths(resource.resourceType) : [];
45328
+ const unorderedPaths = provider.getDriftUnorderedPaths ? provider.getDriftUnorderedPaths(resource.resourceType) : [];
44948
45329
  const useObserved = resource.observedProperties !== void 0;
44949
45330
  const changes = calculateResourceDrift(useObserved ? resource.observedProperties : resource.properties ?? {}, aws, {
44950
45331
  ignorePaths,
44951
- unionWalkObjects: useObserved
45332
+ unionWalkObjects: useObserved,
45333
+ unorderedPaths
44952
45334
  });
44953
45335
  if (changes.length === 0) outcomes.push({
44954
45336
  kind: "clean",
@@ -61758,7 +62140,7 @@ function reorderArgs(argv) {
61758
62140
  async function main() {
61759
62141
  installPipeCloseHandler();
61760
62142
  const program = new Command();
61761
- program.name("cdkd").description("CDK Direct - Deploy AWS CDK apps directly via SDK/Cloud Control API").version("0.251.0");
62143
+ program.name("cdkd").description("CDK Direct - Deploy AWS CDK apps directly via SDK/Cloud Control API").version("0.252.1");
61762
62144
  program.addCommand(createBootstrapCommand());
61763
62145
  program.addCommand(createSynthCommand());
61764
62146
  program.addCommand(createListCommand());