@go-to-k/cdkd 0.251.0 → 0.252.0

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.0";
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
 
@@ -36037,45 +36210,58 @@ var EMRClusterProvider = class {
36037
36210
  * 2. Tag-based lookup: `ListClusters` filtered to the non-terminated states
36038
36211
  * (a `TERMINATED*` cluster is gone and must never be adopted), then a
36039
36212
  * `DescribeCluster` per candidate to read `Tags` (the list summaries do
36040
- * NOT carry tags) and match `aws:cdk:path`.
36213
+ * NOT carry tags) and match `aws:cdk:path`. The walk runs through the
36214
+ * shared `importTagWalk` helper, so each list page / describe is retried
36215
+ * with exponential backoff when AWS throttles the N+1 read burst.
36041
36216
  */
36042
36217
  async import(input) {
36043
36218
  const explicit = resolveExplicitPhysicalId(input, null);
36044
36219
  if (explicit) {
36045
- const cluster = await this.describeClusterOrNull(explicit);
36220
+ const cluster = await this.describeClusterOrUndefined(explicit);
36046
36221
  if (!cluster || cluster.Status?.State && TERMINAL_STATES.has(cluster.Status.State)) return null;
36047
36222
  return {
36048
36223
  physicalId: explicit,
36049
36224
  attributes: this.buildAttributes(cluster)
36050
36225
  };
36051
36226
  }
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)
36227
+ const match = await importTagWalk({
36228
+ cdkPath: input.cdkPath,
36229
+ logicalId: input.logicalId,
36230
+ listPage: async (marker) => {
36231
+ const list = await this.getClient().send(new ListClustersCommand$1({
36232
+ ClusterStates: [...NON_TERMINATED_STATES],
36233
+ ...marker && { Marker: marker }
36234
+ }));
36235
+ return {
36236
+ items: list.Clusters,
36237
+ nextMarker: list.Marker
36066
36238
  };
36067
- }
36068
- marker = list.Marker;
36069
- } while (marker);
36070
- return null;
36239
+ },
36240
+ describe: async (summary) => summary.Id ? await this.describeClusterOrUndefined(summary.Id) : void 0,
36241
+ tagsOf: (cluster) => cluster.Tags
36242
+ });
36243
+ if (!match) return null;
36244
+ return {
36245
+ physicalId: match.summary.Id,
36246
+ attributes: this.buildAttributes(match.detail)
36247
+ };
36071
36248
  }
36072
36249
  /**
36073
36250
  * `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.
36251
+ * cluster id is unknown in this region / aged out of Describe) to
36252
+ * `undefined` instead of throwing, so `import()` can treat it as "no match"
36253
+ * rather than aborting the whole adoption run.
36254
+ *
36255
+ * `undefined`, NOT `null`, is load-bearing: `importTagWalk` skips a candidate
36256
+ * on `detail === undefined`, and an actual `null` would flow into `tagsOf`
36257
+ * and `TypeError`. Also note the mapping is BROAD — `InvalidRequestException`
36258
+ * covers more than not-found, so a genuine failure here degrades to "no
36259
+ * match" and a subsequent deploy would CREATE a duplicate cluster rather than
36260
+ * adopt the existing one. The walk logs each skip at debug so the case is at
36261
+ * least visible under `--verbose`; narrowing the mapping needs a
36262
+ * message/code-level discriminator AWS does not currently document.
36077
36263
  */
36078
- async describeClusterOrNull(clusterId) {
36264
+ async describeClusterOrUndefined(clusterId) {
36079
36265
  try {
36080
36266
  return (await this.getClient().send(new DescribeClusterCommand({ ClusterId: clusterId }))).Cluster;
36081
36267
  } catch (err) {
@@ -36114,7 +36300,7 @@ var EMRClusterProvider = class {
36114
36300
  * optional `import` method's incremental opt-in shape.
36115
36301
  */
36116
36302
  async readCurrentState(physicalId, _logicalId, _resourceType) {
36117
- const cluster = await this.describeClusterOrNull(physicalId);
36303
+ const cluster = await this.describeClusterOrUndefined(physicalId);
36118
36304
  if (!cluster) return void 0;
36119
36305
  let instanceGroups = [];
36120
36306
  let instanceFleets = [];
@@ -36314,14 +36500,21 @@ const DEFAULT_POLL_INTERVAL_MS$1 = 15e3;
36314
36500
  /** Instance-group state that means "the group is up and idle/serving". */
36315
36501
  const READY_STATES$1 = /* @__PURE__ */ new Set(["RUNNING"]);
36316
36502
  /**
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).
36503
+ * Instance-group states that mean "the group will never reach the requested
36504
+ * size" — a hard error during a create/resize wait.
36505
+ *
36506
+ * `ARRESTED` means the group failed to provision; `TERMINATED`/`ENDED` mean it
36507
+ * is gone. `SUSPENDED` means a resize could not complete: the existing
36508
+ * instances keep running but AWS can no longer add or remove any, so the wait
36509
+ * would poll to the full `maxWaitMs` timeout instead of failing fast with the
36510
+ * service's own state-change reason — the exact defect fixed on the
36511
+ * instance-fleet side in issue #1092 item 2, which applies verbatim here.
36320
36512
  */
36321
36513
  const FAILED_STATES$1 = /* @__PURE__ */ new Set([
36322
36514
  "ARRESTED",
36323
- "TERMINATED",
36324
- "ENDED"
36515
+ "ENDED",
36516
+ "SUSPENDED",
36517
+ "TERMINATED"
36325
36518
  ]);
36326
36519
  const toNumber$1 = (v) => {
36327
36520
  if (v === void 0) return void 0;
@@ -36471,7 +36664,7 @@ var EMRInstanceGroupConfigProvider = class {
36471
36664
  InstanceCount: toNumber$1(properties["InstanceCount"])
36472
36665
  }]
36473
36666
  }));
36474
- if (jobFlowId) await this.waitForGroupReady(jobFlowId, physicalId, logicalId, resourceType, toNumber$1(properties["InstanceCount"]) ?? 0);
36667
+ if (jobFlowId) await this.waitForGroupReady(jobFlowId, physicalId, logicalId, resourceType, toNumber$1(properties["InstanceCount"]) ?? 0, true);
36475
36668
  }
36476
36669
  if (autoScalingChanged) {
36477
36670
  if (!jobFlowId) throw new ProvisioningError(`EMR InstanceGroupConfig ${logicalId} AutoScalingPolicy update needs JobFlowId (the cluster id), which is absent`, resourceType, logicalId, physicalId);
@@ -36532,15 +36725,43 @@ var EMRInstanceGroupConfigProvider = class {
36532
36725
  * would return before the resize even starts. Waiting on the running count
36533
36726
  * reaching the requested target settles both create (0 -> N provisioning)
36534
36727
  * and resize (M -> N add/remove) correctly.
36535
- */
36536
- async waitForGroupReady(clusterId, groupId, logicalId, resourceType, expectedCount) {
36728
+ *
36729
+ * `toleratesStaleFailedState` MUST be true on the resize path and false on
36730
+ * create. It exists for the RECOVERY case: `SUSPENDED` (a resize that could
36731
+ * not complete) is precisely the state a user re-runs `cdkd deploy` to fix,
36732
+ * but the same pre-resize read lag documented above means the first poll
36733
+ * after `ModifyInstanceGroups` still reports `SUSPENDED`. Enforcing
36734
+ * {@link FAILED_STATES} on that stale read would abort the very resize that
36735
+ * recovers the group, on every attempt, making the state permanently
36736
+ * unrecoverable through cdkd.
36737
+ *
36738
+ * So on a resize the failed-state check is suppressed until the group is
36739
+ * observed LEAVING whatever state it started in; from then on it is enforced
36740
+ * normally (a resize that transitions RUNNING -> RESIZING -> SUSPENDED still
36741
+ * fails fast, which is the case #1092 item 2 is about). A group that never
36742
+ * leaves its initial state falls through to the `maxWaitMs` timeout — the
36743
+ * pre-fix behavior, and strictly better than an unrecoverable resource.
36744
+ *
36745
+ * Note the asymmetry that makes this specific to failed states: a stale
36746
+ * pre-resize `RUNNING` is harmless because the instance-count mismatch keeps
36747
+ * the loop polling. A stale `SUSPENDED` is terminal.
36748
+ */
36749
+ async waitForGroupReady(clusterId, groupId, logicalId, resourceType, expectedCount, toleratesStaleFailedState = false) {
36537
36750
  const startTime = Date.now();
36538
36751
  const transientState = { count: 0 };
36752
+ let initialState;
36753
+ let hasLeftInitialState = false;
36539
36754
  while (Date.now() - startTime < this.maxWaitMs) {
36540
36755
  const group = await this.findGroupForPoll(clusterId, groupId, transientState);
36541
36756
  const state = group?.Status?.State;
36757
+ if (state) {
36758
+ if (initialState === void 0) initialState = state;
36759
+ else if (state !== initialState) hasLeftInitialState = true;
36760
+ }
36542
36761
  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);
36762
+ const enforceFailedStates = !toleratesStaleFailedState || hasLeftInitialState;
36763
+ 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);
36764
+ 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
36765
  this.logger.debug(`EMR instance group ${groupId} state: ${state ?? "unknown"}, running ${group?.RunningInstanceCount ?? "?"}/${expectedCount}, waiting...`);
36545
36766
  await new Promise((resolve) => setTimeout(resolve, this.pollIntervalMs));
36546
36767
  }
@@ -36596,10 +36817,20 @@ const DEFAULT_POLL_INTERVAL_MS = 15e3;
36596
36817
  /** Instance-fleet state that means "the fleet is up and idle/serving". */
36597
36818
  const READY_STATES = /* @__PURE__ */ new Set(["RUNNING"]);
36598
36819
  /**
36599
- * Instance-fleet states that mean "the fleet is gone" a hard error during a
36600
- * create/resize wait (it will never reach RUNNING).
36820
+ * Instance-fleet states that mean "the fleet will never reach the requested
36821
+ * capacity" a hard error during a create/resize wait.
36822
+ *
36823
+ * `TERMINATED` means the fleet is gone. `SUSPENDED` means a resize could not
36824
+ * complete: the existing instances keep running but AWS can no longer add or
36825
+ * remove any, so the wait would poll to the full `maxWaitMs` timeout instead of
36826
+ * failing fast with the service's own state-change reason (issue #1092 item 2).
36827
+ *
36828
+ * This is the fleet-state ANALOGUE of the instance-group provider's failed set,
36829
+ * not a copy of it: the two enums differ. `ARRESTED` and `ENDED` are
36830
+ * `InstanceGroupState` members that do not exist for fleets, so `TERMINATED` is
36831
+ * the only state the two sets share.
36601
36832
  */
36602
- const FAILED_STATES = /* @__PURE__ */ new Set(["TERMINATED"]);
36833
+ const FAILED_STATES = /* @__PURE__ */ new Set(["SUSPENDED", "TERMINATED"]);
36603
36834
  const toNumber = (v) => {
36604
36835
  if (v === void 0) return void 0;
36605
36836
  const n = Number(v);
@@ -36752,7 +36983,7 @@ var EMRInstanceFleetConfigProvider = class {
36752
36983
  }));
36753
36984
  const newTarget = this.targetCapacity(properties);
36754
36985
  const prevTarget = this.targetCapacity(previousProperties);
36755
- await this.waitForFleetReady(clusterId, physicalId, logicalId, resourceType, newTarget, newTarget >= prevTarget);
36986
+ await this.waitForFleetReady(clusterId, physicalId, logicalId, resourceType, newTarget, newTarget >= prevTarget, true);
36756
36987
  this.logger.debug(`Successfully updated EMR instance fleet ${logicalId}`);
36757
36988
  return {
36758
36989
  physicalId,
@@ -36812,17 +37043,45 @@ var EMRInstanceFleetConfigProvider = class {
36812
37043
  *
36813
37044
  * A `targetCapacity` of 0 on the up path (never valid for a real create —
36814
37045
  * at least one target must be > 0) degrades to a State-only wait.
36815
- */
36816
- async waitForFleetReady(clusterId, fleetId, logicalId, resourceType, targetCapacity, atLeast) {
37046
+ *
37047
+ * `toleratesStaleFailedState` MUST be true on the resize path and false on
37048
+ * create. It exists for the RECOVERY case: `SUSPENDED` (a resize that could
37049
+ * not complete) is precisely the state a user re-runs `cdkd deploy` to fix,
37050
+ * but the same pre-resize read lag documented above means the first poll
37051
+ * after `ModifyInstanceFleet` still reports `SUSPENDED`. Enforcing
37052
+ * {@link FAILED_STATES} on that stale read would abort the very resize that
37053
+ * recovers the fleet, on every attempt, making the state permanently
37054
+ * unrecoverable through cdkd.
37055
+ *
37056
+ * So on a resize the failed-state check is suppressed until the fleet is
37057
+ * observed LEAVING whatever state it started in; from then on it is enforced
37058
+ * normally (a resize that transitions RUNNING -> RESIZING -> SUSPENDED still
37059
+ * fails fast, which is the case #1092 item 2 is about). A fleet that never
37060
+ * leaves its initial state falls through to the `maxWaitMs` timeout — the
37061
+ * pre-fix behavior, and strictly better than an unrecoverable resource.
37062
+ *
37063
+ * Note the asymmetry that makes this specific to failed states: a stale
37064
+ * pre-resize `RUNNING` is harmless because the capacity check keeps the loop
37065
+ * polling. A stale `SUSPENDED` is terminal.
37066
+ */
37067
+ async waitForFleetReady(clusterId, fleetId, logicalId, resourceType, targetCapacity, atLeast, toleratesStaleFailedState = false) {
36817
37068
  const startTime = Date.now();
36818
37069
  const transientState = { count: 0 };
37070
+ let initialState;
37071
+ let hasLeftInitialState = false;
36819
37072
  while (Date.now() - startTime < this.maxWaitMs) {
36820
37073
  const fleet = await this.findFleetForPoll(clusterId, fleetId, transientState);
36821
37074
  const state = fleet?.Status?.State;
36822
37075
  const provisioned = (fleet?.ProvisionedOnDemandCapacity ?? 0) + (fleet?.ProvisionedSpotCapacity ?? 0);
36823
37076
  const capacityReady = atLeast ? provisioned >= targetCapacity : provisioned <= targetCapacity;
37077
+ if (state) {
37078
+ if (initialState === void 0) initialState = state;
37079
+ else if (state !== initialState) hasLeftInitialState = true;
37080
+ }
36824
37081
  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);
37082
+ const enforceFailedStates = !toleratesStaleFailedState || hasLeftInitialState;
37083
+ 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);
37084
+ 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
37085
  this.logger.debug(`EMR instance fleet ${fleetId} state: ${state ?? "unknown"}, provisioned ${provisioned}/${targetCapacity}, waiting...`);
36827
37086
  await new Promise((resolve) => setTimeout(resolve, this.pollIntervalMs));
36828
37087
  }
@@ -61758,7 +62017,7 @@ function reorderArgs(argv) {
61758
62017
  async function main() {
61759
62018
  installPipeCloseHandler();
61760
62019
  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");
62020
+ program.name("cdkd").description("CDK Direct - Deploy AWS CDK apps directly via SDK/Cloud Control API").version("0.252.0");
61762
62021
  program.addCommand(createBootstrapCommand());
61763
62022
  program.addCommand(createSynthCommand());
61764
62023
  program.addCommand(createListCommand());