@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/{asg-provider-er3xGYRo.js → asg-provider-BzBdoSYp.js} +2 -2
- package/dist/{asg-provider-er3xGYRo.js.map → asg-provider-BzBdoSYp.js.map} +1 -1
- package/dist/cli.js +474 -92
- package/dist/cli.js.map +1 -1
- package/dist/{deploy-engine-D-gBbMiZ.js → deploy-engine-IkhnJXWR.js} +25 -17
- package/dist/deploy-engine-IkhnJXWR.js.map +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/package.json +1 -1
- package/dist/deploy-engine-D-gBbMiZ.js.map +0 -1
package/dist/cli.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { $ as
|
|
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-
|
|
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.
|
|
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
|
-
|
|
25815
|
-
|
|
25816
|
-
|
|
25817
|
-
|
|
25818
|
-
|
|
25819
|
-
|
|
25820
|
-
|
|
25821
|
-
|
|
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
|
-
|
|
25826
|
-
|
|
25827
|
-
|
|
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
|
-
|
|
25842
|
-
|
|
25843
|
-
|
|
25844
|
-
|
|
25845
|
-
|
|
25846
|
-
|
|
25847
|
-
|
|
25848
|
-
|
|
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
|
-
|
|
25853
|
-
|
|
25854
|
-
|
|
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
|
-
|
|
25869
|
-
|
|
25870
|
-
|
|
25871
|
-
|
|
25872
|
-
|
|
25873
|
-
|
|
25874
|
-
|
|
25875
|
-
|
|
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
|
-
|
|
25880
|
-
|
|
25881
|
-
|
|
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.
|
|
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
|
-
|
|
36053
|
-
|
|
36054
|
-
|
|
36055
|
-
|
|
36056
|
-
|
|
36057
|
-
|
|
36058
|
-
|
|
36059
|
-
|
|
36060
|
-
|
|
36061
|
-
|
|
36062
|
-
|
|
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
|
-
|
|
36069
|
-
|
|
36070
|
-
|
|
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
|
|
36075
|
-
* instead of throwing, so `import()` can treat it as "no match"
|
|
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
|
|
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.
|
|
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
|
|
36318
|
-
* error during a create/resize wait
|
|
36319
|
-
*
|
|
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
|
-
"
|
|
36324
|
-
"
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
36600
|
-
*
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
44534
|
-
|
|
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
|
-
|
|
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.
|
|
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());
|