@go-to-k/cdkd 0.281.22 → 0.281.24
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/README.md +16 -1
- package/dist/{asg-provider-pvAc9MXZ.js → asg-provider-CxK7d3pH.js} +2 -2
- package/dist/{asg-provider-pvAc9MXZ.js.map → asg-provider-CxK7d3pH.js.map} +1 -1
- package/dist/cli.js +399 -50
- package/dist/cli.js.map +1 -1
- package/dist/{deploy-engine-DQd1PUUv.js → deploy-engine-BkXNtI7L.js} +12 -3
- package/dist/{deploy-engine-DQd1PUUv.js.map → deploy-engine-BkXNtI7L.js.map} +1 -1
- package/dist/index.d.ts +11 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { $ as configStringRefusal, $t as MIGRATE_TMP_PREFIX, A as green, At as validateContainerRepoName, B as slowCcOperationTimeoutMs, Bt as getDefaultStateBucketName, C as MULTI_REGION_RECREATE_BLOCKED_TYPES, Cn as PartialFailureError, Ct as rewriteTemplateAssetReferences, D as bold, Dn as StackHasActiveImportsError, Dt as getBootstrapMarkerKey, E as formatResourceLine, En as ResourceUpdateNotSupportedError, Et as ensureAssetStorage, F as clearOnUpdateRemoval, Ft as runDockerStreaming, G as getAccountInfo, Gt as resolveSkipPrefix, H as isTerminationProtectionPropagationError, Ht as resolveApp, I as ProviderRegistry, It as AssetManifestLoader, J as normalizeAwsTagsToCfn, Jt as resolveUseCdkBootstrapAssets, K as refStateLookupFromResource, Kt as resolveStateBucketWithDefault, L as findActionableSilentDrops, Lt as getDockerImageBySourceHash, M as yellow, Mt as formatDockerLoginError, N as IAMRoleProvider, Nn as normalizeAwsError, Nt as getDockerCmd, O as cyan, On as StackTerminationProtectionError, Ot as parseBootstrapMarker, P as collectInlinePolicyNamesManagedBySiblings, Pn as withErrorHandling, Pt as runDockerForeground, Q as configBooleanRefusal, Qt as CFN_TEMPLATE_URL_LIMIT, R as findSilentDropProperties, Rt as Synthesizer, S as extractDeploymentEventError, Sn as NestedStackChildDirectDestroyError, St as loadPublishableAssetManifest, T as renderStatefulReason, Tn as ResourceTimeoutError, Tt as BOOTSTRAP_MARKER_PREFIX, U as IntrinsicFunctionResolver, Ut as resolveAutoAssetStorage, V as disableInstanceApiTermination, Vt as getLegacyStateBucketName, W as cfnRefValueFromPhysicalId, Wt as resolveCaptureObservedState, X as assertRegionMatch, Xt as warnDeprecatedNoPrefixCliFlag, Y as resolveExplicitPhysicalId, Yt as stateBucketExistenceConfirmed, Z as coerceCfnBoolean, Zt as CFN_TEMPLATE_BODY_LIMIT, _ as createPreDeleteFinalSnapshot, _n as LocalInvokeBuildError$1, _t as AssetPublisher, a as DeploymentEventsStore, an as processStackMessages, at as applyRoleArnIfSet, b as unsupportedFinalSnapshotError, bt as buildAssetRedirectMap, c as replayFailedOperations, cn as AwsClients, ct as withRetry, d as IMPLICIT_DELETE_DEPENDENCIES, dn as setAwsClients, dt as DagBuilder, en as findLargeInlineResources, et as readConfigString, f as computeImplicitDeleteEdges, ft as TemplateParser, g as ccRoutedFinalSnapshotError, gn as DeployCancelledError, gt as shouldRetainResource, h as buildFinalSnapshotIdentifier, ht as rebuildClientForBucketRegion, i as DeploymentEventsReader, in as AssemblyReader, it as requireConfigString, j as red, jt as buildDockerImage, k as gray, kt as validateAssetBucketName, l as replayRollback, ln as getAwsClients, lt as isRetryableTransientError, m as PRE_DELETE_SNAPSHOT_TYPES, mt as S3StateBackend, n as DEFAULT_RESOURCE_WARN_AFTER_MS, nn as expectedOwnerParam, nt as requireConfigArray, o as planFailedOps, ot as DiffCalculator, p as ATOMIC_FINAL_SNAPSHOT_TYPES, pn as CdkdError, pt as LockManager, q as WAFv2WebACLProvider, qt as resolveStateBucketWithDefaultAndSource, r as DeployEngine, rn as derivePartitionAndUrlSuffix, rt as requireConfigObject, s as planRollback, sn as resolveBucketRegion, st as describeTypeWithThrottleRetry, t as DEFAULT_RESOURCE_TIMEOUT_MS, tn as uploadCfnTemplate, tt as replayWarn, u as withResourceDeadline, un as resetAwsClients, ut as isThrottlingError, v as isFinalSnapshotError, vn as LocalMigrateError, vt as stringifyValue, w as isStatefulRecreateTargetSync, wn as ProvisioningError, wt as AssetModeResolver, x as makeCanonicalizePropertiesFn, xn as MissingCdkCliError, xt as createAssetRedirectResolver, y as refusesFinalSnapshot, yn as LocalStartServiceError, yt as WorkGraph, z as CloudControlProvider, zt as synthesisStatusMessage } from "./deploy-engine-
|
|
2
|
+
import { $ as configStringRefusal, $t as MIGRATE_TMP_PREFIX, A as green, At as validateContainerRepoName, B as slowCcOperationTimeoutMs, Bt as getDefaultStateBucketName, C as MULTI_REGION_RECREATE_BLOCKED_TYPES, Cn as PartialFailureError, Ct as rewriteTemplateAssetReferences, D as bold, Dn as StackHasActiveImportsError, Dt as getBootstrapMarkerKey, E as formatResourceLine, En as ResourceUpdateNotSupportedError, Et as ensureAssetStorage, F as clearOnUpdateRemoval, Ft as runDockerStreaming, G as getAccountInfo, Gt as resolveSkipPrefix, H as isTerminationProtectionPropagationError, Ht as resolveApp, I as ProviderRegistry, It as AssetManifestLoader, J as normalizeAwsTagsToCfn, Jt as resolveUseCdkBootstrapAssets, K as refStateLookupFromResource, Kt as resolveStateBucketWithDefault, L as findActionableSilentDrops, Lt as getDockerImageBySourceHash, M as yellow, Mt as formatDockerLoginError, N as IAMRoleProvider, Nn as normalizeAwsError, Nt as getDockerCmd, O as cyan, On as StackTerminationProtectionError, Ot as parseBootstrapMarker, P as collectInlinePolicyNamesManagedBySiblings, Pn as withErrorHandling, Pt as runDockerForeground, Q as configBooleanRefusal, Qt as CFN_TEMPLATE_URL_LIMIT, R as findSilentDropProperties, Rt as Synthesizer, S as extractDeploymentEventError, Sn as NestedStackChildDirectDestroyError, St as loadPublishableAssetManifest, T as renderStatefulReason, Tn as ResourceTimeoutError, Tt as BOOTSTRAP_MARKER_PREFIX, U as IntrinsicFunctionResolver, Ut as resolveAutoAssetStorage, V as disableInstanceApiTermination, Vt as getLegacyStateBucketName, W as cfnRefValueFromPhysicalId, Wt as resolveCaptureObservedState, X as assertRegionMatch, Xt as warnDeprecatedNoPrefixCliFlag, Y as resolveExplicitPhysicalId, Yt as stateBucketExistenceConfirmed, Z as coerceCfnBoolean, Zt as CFN_TEMPLATE_BODY_LIMIT, _ as createPreDeleteFinalSnapshot, _n as LocalInvokeBuildError$1, _t as AssetPublisher, a as DeploymentEventsStore, an as processStackMessages, at as applyRoleArnIfSet, b as unsupportedFinalSnapshotError, bt as buildAssetRedirectMap, c as replayFailedOperations, cn as AwsClients, ct as withRetry, d as IMPLICIT_DELETE_DEPENDENCIES, dn as setAwsClients, dt as DagBuilder, en as findLargeInlineResources, et as readConfigString, f as computeImplicitDeleteEdges, ft as TemplateParser, g as ccRoutedFinalSnapshotError, gn as DeployCancelledError, gt as shouldRetainResource, h as buildFinalSnapshotIdentifier, ht as rebuildClientForBucketRegion, i as DeploymentEventsReader, in as AssemblyReader, it as requireConfigString, j as red, jt as buildDockerImage, k as gray, kt as validateAssetBucketName, l as replayRollback, ln as getAwsClients, lt as isRetryableTransientError, m as PRE_DELETE_SNAPSHOT_TYPES, mt as S3StateBackend, n as DEFAULT_RESOURCE_WARN_AFTER_MS, nn as expectedOwnerParam, nt as requireConfigArray, o as planFailedOps, ot as DiffCalculator, p as ATOMIC_FINAL_SNAPSHOT_TYPES, pn as CdkdError, pt as LockManager, q as WAFv2WebACLProvider, qt as resolveStateBucketWithDefaultAndSource, r as DeployEngine, rn as derivePartitionAndUrlSuffix, rt as requireConfigObject, s as planRollback, sn as resolveBucketRegion, st as describeTypeWithThrottleRetry, t as DEFAULT_RESOURCE_TIMEOUT_MS, tn as uploadCfnTemplate, tt as replayWarn, u as withResourceDeadline, un as resetAwsClients, ut as isThrottlingError, v as isFinalSnapshotError, vn as LocalMigrateError, vt as stringifyValue, w as isStatefulRecreateTargetSync, wn as ProvisioningError, wt as AssetModeResolver, x as makeCanonicalizePropertiesFn, xn as MissingCdkCliError, xt as createAssetRedirectResolver, y as refusesFinalSnapshot, yn as LocalStartServiceError, yt as WorkGraph, z as CloudControlProvider, zt as synthesisStatusMessage } from "./deploy-engine-BkXNtI7L.js";
|
|
3
3
|
import { a as getLiveRenderer, c as PATTERN_B_RESOURCE_TYPES, d as generateResourceNameWithFallback, h as withStackName, i as runStackBuffered, m as withSkipPrefix, n as getLogger, o as PATTERN_B_NAME_OPTIONS, s as PATTERN_B_NAME_PROPERTIES, u as generateResourceName } from "./logger-zRrlbaQt.js";
|
|
4
|
-
import { t as ASGProvider } from "./asg-provider-
|
|
4
|
+
import { t as ASGProvider } from "./asg-provider-CxK7d3pH.js";
|
|
5
5
|
import { AsyncLocalStorage } from "node:async_hooks";
|
|
6
6
|
import { createHash, randomBytes, randomUUID } from "node:crypto";
|
|
7
7
|
import { CopyObjectCommand, CreateBucketCommand, DeleteBucketAnalyticsConfigurationCommand, DeleteBucketCommand, DeleteBucketCorsCommand, DeleteBucketEncryptionCommand, DeleteBucketIntelligentTieringConfigurationCommand, DeleteBucketInventoryConfigurationCommand, DeleteBucketLifecycleCommand, DeleteBucketMetricsConfigurationCommand, DeleteBucketOwnershipControlsCommand, DeleteBucketPolicyCommand, DeleteBucketReplicationCommand, DeleteBucketTaggingCommand, DeleteBucketWebsiteCommand, DeleteObjectsCommand, GetBucketAccelerateConfigurationCommand, GetBucketCorsCommand, GetBucketEncryptionCommand, GetBucketLifecycleConfigurationCommand, GetBucketLocationCommand, GetBucketLoggingCommand, GetBucketNotificationConfigurationCommand, GetBucketOwnershipControlsCommand, GetBucketPolicyCommand, GetBucketReplicationCommand, GetBucketTaggingCommand, GetBucketVersioningCommand, GetBucketWebsiteCommand, GetObjectCommand, GetObjectLockConfigurationCommand, GetPublicAccessBlockCommand, HeadBucketCommand, ListBucketAnalyticsConfigurationsCommand, ListBucketIntelligentTieringConfigurationsCommand, ListBucketInventoryConfigurationsCommand, ListBucketMetricsConfigurationsCommand, ListObjectVersionsCommand, ListObjectsV2Command, NoSuchBucket, PutBucketAccelerateConfigurationCommand, PutBucketAnalyticsConfigurationCommand, PutBucketCorsCommand, PutBucketEncryptionCommand, PutBucketIntelligentTieringConfigurationCommand, PutBucketInventoryConfigurationCommand, PutBucketLifecycleConfigurationCommand, PutBucketLoggingCommand, PutBucketMetricsConfigurationCommand, PutBucketNotificationConfigurationCommand, PutBucketOwnershipControlsCommand, PutBucketPolicyCommand, PutBucketReplicationCommand, PutBucketTaggingCommand, PutBucketVersioningCommand, PutBucketWebsiteCommand, PutObjectCommand, PutObjectLockConfigurationCommand, PutPublicAccessBlockCommand, S3Client, S3ServiceException } from "@aws-sdk/client-s3";
|
|
@@ -12455,16 +12455,57 @@ function formatShape(segments) {
|
|
|
12455
12455
|
* ## The `skipping` variant
|
|
12456
12456
|
*
|
|
12457
12457
|
* On the DELETE path the established policy is warn-and-continue, so a
|
|
12458
|
-
* malformed record leaves the AWS resource ALIVE
|
|
12459
|
-
*
|
|
12460
|
-
*
|
|
12461
|
-
*
|
|
12458
|
+
* malformed record leaves the AWS resource ALIVE. That policy is unchanged —
|
|
12459
|
+
* the warning says what the skip costs, because an orphan the user is never
|
|
12460
|
+
* told about is one they cannot go and delete.
|
|
12461
|
+
*
|
|
12462
|
+
* Since issue [#1752](https://github.com/go-to-k/cdkd/issues/1752) the ACCOUNTING
|
|
12463
|
+
* around it is no longer a lie: an arm emitting this message must also return
|
|
12464
|
+
* `{ outcome: 'skipped', reason: COMPOSITE_ID_SKIP_REASON }` so `cdkd destroy`
|
|
12465
|
+
* prints a `⚠ … skipped` line instead of `✓ … deleted`, reports
|
|
12466
|
+
* `N deleted, 1 skipped`, KEEPS the state record, and exits non-zero. The
|
|
12467
|
+
* wording here changed accordingly — it used to promise unconditionally that
|
|
12468
|
+
* cdkd "will report this delete as successful", which is no longer true on the
|
|
12469
|
+
* destroy path. It is STILL true for the deploy engine's replacement / rollback
|
|
12470
|
+
* deletes, which discard the return value (issue
|
|
12471
|
+
* [#1762](https://github.com/go-to-k/cdkd/issues/1762)), so the message names
|
|
12472
|
+
* both callers rather than describing only the fixed one — a remedy of
|
|
12473
|
+
* "drop the record with 'cdkd state orphan'" is impossible on a path that has
|
|
12474
|
+
* already dropped it.
|
|
12462
12475
|
*/
|
|
12463
12476
|
function compositeIdFormatMessage(format, logicalId, physicalId, options) {
|
|
12464
12477
|
const accepted = format.alsoAccepts ? `"${formatShape(format.segments)}" or ${format.alsoAccepts}` : `"${formatShape(format.segments)}"`;
|
|
12465
12478
|
const head = `Invalid physicalId format for ${format.label} ${logicalId}: expected ${accepted}, got "${physicalId}"`;
|
|
12466
12479
|
if (!options?.skipping) return head;
|
|
12467
|
-
return `${head}. Skipping the delete —
|
|
12480
|
+
return `${head}. Skipping the delete — no AWS call is issued, so the resource is LEFT IN PLACE. On 'cdkd destroy' / 'cdkd state destroy' the state record is KEPT and the run exits non-zero, so repair the id in state.json and re-run (or delete the resource by hand and drop the record with 'cdkd state orphan'). NOTE this arm is ALSO reached from cdkd deploy — the plain DELETE of a resource removed from the template (the most common one), plus the replacement and rollback deletes — all of which still DROP the record and report success (https://github.com/go-to-k/cdkd/issues/1762) — there, delete it by hand.`;
|
|
12481
|
+
}
|
|
12482
|
+
/**
|
|
12483
|
+
* The short `ResourceDeleteResult.reason` every malformed-composite-id DELETE
|
|
12484
|
+
* skip reports (issue [#1752](https://github.com/go-to-k/cdkd/issues/1752)).
|
|
12485
|
+
*
|
|
12486
|
+
* Deliberately short and IDENTICAL across the five arms: it is rendered inline
|
|
12487
|
+
* on the per-resource status line (`⚠ MyTable (AWS::Glue::Table) skipped
|
|
12488
|
+
* (malformed physicalId in state — no delete issued)`), where the full
|
|
12489
|
+
* remediation sentence built by {@link compositeIdFormatMessage} would not fit.
|
|
12490
|
+
* The full text is still emitted, as the `logger.warn` right beside it.
|
|
12491
|
+
*
|
|
12492
|
+
* A shared constant rather than five literals so the five arms cannot drift and
|
|
12493
|
+
* a grep for the wording finds all of them.
|
|
12494
|
+
*/
|
|
12495
|
+
const COMPOSITE_ID_SKIP_REASON = "malformed physicalId in state — no delete issued";
|
|
12496
|
+
/**
|
|
12497
|
+
* The `ResourceDeleteResult` a malformed-composite-id DELETE arm returns.
|
|
12498
|
+
*
|
|
12499
|
+
* Pairs with the `{ skipping: true }` warning: emit the warning, return this.
|
|
12500
|
+
* Providers call it as `return compositeIdSkipResult();` so the arm reads as
|
|
12501
|
+
* the deliberate skip it is rather than as the bare `return;` that made issue
|
|
12502
|
+
* #1752's mis-accounting possible.
|
|
12503
|
+
*/
|
|
12504
|
+
function compositeIdSkipResult() {
|
|
12505
|
+
return {
|
|
12506
|
+
outcome: "skipped",
|
|
12507
|
+
reason: COMPOSITE_ID_SKIP_REASON
|
|
12508
|
+
};
|
|
12468
12509
|
}
|
|
12469
12510
|
|
|
12470
12511
|
//#endregion
|
|
@@ -21890,7 +21931,7 @@ var EC2Provider = class {
|
|
|
21890
21931
|
const parts = physicalId.split("|");
|
|
21891
21932
|
if (parts.length < 3) {
|
|
21892
21933
|
this.logger.warn(compositeIdFormatMessage(EC2_NETWORK_ACL_ENTRY_ID_FORMAT, logicalId, physicalId, { skipping: true }));
|
|
21893
|
-
return;
|
|
21934
|
+
return compositeIdSkipResult();
|
|
21894
21935
|
}
|
|
21895
21936
|
const networkAclId = parts[0];
|
|
21896
21937
|
const ruleNumber = parseInt(parts[1], 10);
|
|
@@ -39028,7 +39069,7 @@ var AppSyncProvider = class {
|
|
|
39028
39069
|
const [apiId, name] = physicalId.split("|");
|
|
39029
39070
|
if (!apiId || !name) {
|
|
39030
39071
|
this.logger.warn(compositeIdFormatMessage(APPSYNC_DATASOURCE_ID_FORMAT, logicalId, physicalId, { skipping: true }));
|
|
39031
|
-
return;
|
|
39072
|
+
return compositeIdSkipResult();
|
|
39032
39073
|
}
|
|
39033
39074
|
try {
|
|
39034
39075
|
await this.getClient().send(new DeleteDataSourceCommand({
|
|
@@ -39091,7 +39132,7 @@ var AppSyncProvider = class {
|
|
|
39091
39132
|
const parts = physicalId.split("|");
|
|
39092
39133
|
if (parts.length < 3) {
|
|
39093
39134
|
this.logger.warn(compositeIdFormatMessage(APPSYNC_RESOLVER_ID_FORMAT, logicalId, physicalId, { skipping: true }));
|
|
39094
|
-
return;
|
|
39135
|
+
return compositeIdSkipResult();
|
|
39095
39136
|
}
|
|
39096
39137
|
const [apiId, typeName, fieldName] = parts;
|
|
39097
39138
|
try {
|
|
@@ -39147,7 +39188,7 @@ var AppSyncProvider = class {
|
|
|
39147
39188
|
const [apiId, apiKeyId] = physicalId.split("|");
|
|
39148
39189
|
if (!apiId || !apiKeyId) {
|
|
39149
39190
|
this.logger.warn(compositeIdFormatMessage(APPSYNC_APIKEY_ID_FORMAT, logicalId, physicalId, { skipping: true }));
|
|
39150
|
-
return;
|
|
39191
|
+
return compositeIdSkipResult();
|
|
39151
39192
|
}
|
|
39152
39193
|
try {
|
|
39153
39194
|
await this.getClient().send(new DeleteApiKeyCommand({
|
|
@@ -39873,7 +39914,7 @@ var GlueProvider = class {
|
|
|
39873
39914
|
const [databaseName, tableName] = physicalId.split("|");
|
|
39874
39915
|
if (!databaseName || !tableName) {
|
|
39875
39916
|
this.logger.warn(compositeIdFormatMessage(GLUE_TABLE_ID_FORMAT, logicalId, physicalId, { skipping: true }));
|
|
39876
|
-
return;
|
|
39917
|
+
return compositeIdSkipResult();
|
|
39877
39918
|
}
|
|
39878
39919
|
const target = deleteCatalogId(properties);
|
|
39879
39920
|
if (target.declaredButUnusable) this.logger.debug(`Glue Table ${logicalId}: CatalogId is not a usable value (likely an unresolved intrinsic), so DeleteTable targets ${describeCatalog(target.catalogId)}.`);
|
|
@@ -52076,6 +52117,12 @@ var ECRProvider = class {
|
|
|
52076
52117
|
*
|
|
52077
52118
|
* Exported for unit-test coverage of `countProtectedResources`.
|
|
52078
52119
|
*/
|
|
52120
|
+
/**
|
|
52121
|
+
* The nested-stack resource type. A skipped row of this type means the CHILD
|
|
52122
|
+
* stack was not destroyed, and the record a user must repair lives in the
|
|
52123
|
+
* child's own state file (`<parent>~<logicalId>`) — see `skippedStateTargets`.
|
|
52124
|
+
*/
|
|
52125
|
+
const NESTED_STACK_TYPE = "AWS::CloudFormation::Stack";
|
|
52079
52126
|
const PROTECTION_PROPERTY_BY_TYPE = {
|
|
52080
52127
|
"AWS::Logs::LogGroup": "DeletionProtectionEnabled",
|
|
52081
52128
|
"AWS::RDS::DBInstance": "DeletionProtection",
|
|
@@ -52179,6 +52226,7 @@ async function runDestroyForStack(stackName, state, ctx) {
|
|
|
52179
52226
|
skippedEmpty: false,
|
|
52180
52227
|
deletedCount: 0,
|
|
52181
52228
|
retainedCount: 0,
|
|
52229
|
+
skippedCount: 0,
|
|
52182
52230
|
errorCount: 0,
|
|
52183
52231
|
interrupted: false
|
|
52184
52232
|
};
|
|
@@ -52285,6 +52333,7 @@ async function runDestroyForStack(stackName, state, ctx) {
|
|
|
52285
52333
|
}
|
|
52286
52334
|
}
|
|
52287
52335
|
const remainingResources = { ...state.resources };
|
|
52336
|
+
const skippedStateTargets = /* @__PURE__ */ new Set();
|
|
52288
52337
|
const buildDestroySnapshot = () => {
|
|
52289
52338
|
const { imports: _imports, outputReads: _outputReads, ...rest } = state;
|
|
52290
52339
|
return {
|
|
@@ -52412,15 +52461,16 @@ async function runDestroyForStack(stackName, state, ctx) {
|
|
|
52412
52461
|
const globalTimeoutMs = ctx.resourceTimeoutMs ?? 18e5;
|
|
52413
52462
|
const slowTypeMinTimeoutMs = slowCcOperationTimeoutMs(resource.resourceType, "DELETE");
|
|
52414
52463
|
const timeoutMs = ctx.resourceTimeoutByType?.[resource.resourceType] ?? Math.max(providerMinTimeoutMs, slowTypeMinTimeoutMs, globalTimeoutMs);
|
|
52464
|
+
let deleteResult;
|
|
52415
52465
|
await withResourceDeadline(async () => {
|
|
52416
52466
|
const maxAttempts = provider.disableOuterRetry ? 0 : 3;
|
|
52417
52467
|
let lastDeleteError;
|
|
52418
52468
|
for (let attempt = 0; attempt <= maxAttempts; attempt++) try {
|
|
52419
|
-
await provider.delete(logicalId, resource.physicalId, resource.resourceType, resource.properties, {
|
|
52469
|
+
deleteResult = await provider.delete(logicalId, resource.physicalId, resource.resourceType, resource.properties, {
|
|
52420
52470
|
...state.region !== void 0 && { expectedRegion: state.region },
|
|
52421
52471
|
...ctx.removeProtection === true && { removeProtection: true },
|
|
52422
52472
|
...finalSnapshotIdentifier !== void 0 && { finalSnapshotIdentifier }
|
|
52423
|
-
});
|
|
52473
|
+
}) ?? void 0;
|
|
52424
52474
|
lastDeleteError = null;
|
|
52425
52475
|
break;
|
|
52426
52476
|
} catch (retryError) {
|
|
@@ -52445,6 +52495,23 @@ async function runDestroyForStack(stackName, state, ctx) {
|
|
|
52445
52495
|
onTimeout: (elapsedMs) => new ResourceTimeoutError(logicalId, resource.resourceType, stackRegion, elapsedMs, "DELETE", timeoutMs)
|
|
52446
52496
|
});
|
|
52447
52497
|
renderer.removeTask(logicalId);
|
|
52498
|
+
if (deleteResult?.outcome === "skipped") {
|
|
52499
|
+
logger.info(` ${formatResourceLine("skipped", logicalId, resource.resourceType, `skipped (${deleteResult.reason})`)}`);
|
|
52500
|
+
result.skippedCount++;
|
|
52501
|
+
skippedStateTargets.add(resource.resourceType === NESTED_STACK_TYPE ? `${stackName}~${logicalId}` : stackName);
|
|
52502
|
+
ctx.eventRecorder?.record({
|
|
52503
|
+
eventType: "RESOURCE_SKIPPED",
|
|
52504
|
+
stackName,
|
|
52505
|
+
operation: "DELETE",
|
|
52506
|
+
logicalId,
|
|
52507
|
+
resourceType: resource.resourceType,
|
|
52508
|
+
...resource.provisionedBy && { provisionedBy: resource.provisionedBy },
|
|
52509
|
+
...resource.physicalId && { physicalId: resource.physicalId },
|
|
52510
|
+
reason: deleteResult.reason,
|
|
52511
|
+
durationMs: Date.now() - resourceStartedAt
|
|
52512
|
+
});
|
|
52513
|
+
return;
|
|
52514
|
+
}
|
|
52448
52515
|
logger.info(` ${formatResourceLine("deleted", logicalId, resource.resourceType)}`);
|
|
52449
52516
|
result.deletedCount++;
|
|
52450
52517
|
ctx.eventRecorder?.record({
|
|
@@ -52513,7 +52580,7 @@ async function runDestroyForStack(stackName, state, ctx) {
|
|
|
52513
52580
|
}
|
|
52514
52581
|
result.interrupted = draining;
|
|
52515
52582
|
await saveChain;
|
|
52516
|
-
const preserveState = result.errorCount > 0 || result.interrupted;
|
|
52583
|
+
const preserveState = result.errorCount > 0 || result.interrupted || result.skippedCount > 0;
|
|
52517
52584
|
if (!preserveState) {
|
|
52518
52585
|
await ctx.stateBackend.deleteState(stackName, regionForState);
|
|
52519
52586
|
logger.debug("State deleted");
|
|
@@ -52525,12 +52592,19 @@ async function runDestroyForStack(stackName, state, ctx) {
|
|
|
52525
52592
|
logger.warn(`Failed to persist remaining state after partial destroy: ${error instanceof Error ? error.message : String(error)}. The state file may still list already-deleted resources; a re-run resolves them idempotently.`);
|
|
52526
52593
|
}
|
|
52527
52594
|
if (result.interrupted) logger.warn(`Destroy interrupted — ${Object.keys(remainingResources).length} resource(s) not deleted. State preserved.`);
|
|
52528
|
-
else logger.warn(`${result.errorCount} resource(s) failed to delete. State preserved.`);
|
|
52595
|
+
else if (result.errorCount > 0) logger.warn(`${result.errorCount} resource(s) failed to delete. State preserved.`);
|
|
52596
|
+
else logger.warn(`${result.skippedCount} resource(s) skipped — cdkd could not address them, so no delete was issued and they may still exist in AWS. State preserved (the records are kept so the resources stay traceable).`);
|
|
52529
52597
|
}
|
|
52530
52598
|
const retainedSuffix = result.retainedCount > 0 ? `, ${result.retainedCount} retained` : "";
|
|
52599
|
+
const skippedSuffix = result.skippedCount > 0 ? `, ${yellow(result.skippedCount)} skipped` : "";
|
|
52531
52600
|
if (!preserveState) logger.info(`\n${green("✓")} ${bold(`Stack ${stackName} destroyed`)} (${green(result.deletedCount)} deleted${retainedSuffix}, ${result.errorCount} errors)`);
|
|
52532
|
-
else if (result.interrupted && result.errorCount === 0) logger.warn(`\n${yellow("⚠")} ${bold(`Stack ${stackName} destroy interrupted`)} (${green(result.deletedCount)} deleted${retainedSuffix}, ${result.errorCount} errors). State preserved — re-run 'cdkd destroy' / 'cdkd state destroy' to finish.`);
|
|
52533
|
-
else
|
|
52601
|
+
else if (result.interrupted && result.errorCount === 0) logger.warn(`\n${yellow("⚠")} ${bold(`Stack ${stackName} destroy interrupted`)} (${green(result.deletedCount)} deleted${retainedSuffix}${skippedSuffix}, ${result.errorCount} errors). State preserved — re-run 'cdkd destroy' / 'cdkd state destroy' to finish.`);
|
|
52602
|
+
else if (result.errorCount === 0) {
|
|
52603
|
+
const targets = [...skippedStateTargets];
|
|
52604
|
+
const showHint = targets.map((t) => `'cdkd state show ${t}'`).join(" / ");
|
|
52605
|
+
const orphanHint = targets.map((t) => `'cdkd state orphan ${t}'`).join(" / ");
|
|
52606
|
+
logger.warn(`\n${yellow("⚠")} ${bold(`Stack ${stackName} partially destroyed`)} (${green(result.deletedCount)} deleted${retainedSuffix}${skippedSuffix}, ${result.errorCount} errors). cdkd could not address the skipped resource(s), so they may still exist in AWS. Fix the physicalId in state.json (${showHint}) and re-run, or delete them by hand and drop the records with ${orphanHint}.`);
|
|
52607
|
+
} else logger.warn(`\n${yellow("⚠")} ${bold(`Stack ${stackName} partially destroyed`)} (${green(result.deletedCount)} deleted${retainedSuffix}${skippedSuffix}, ${red(result.errorCount)} errors). State preserved — re-run 'cdkd destroy' / 'cdkd state destroy' to clean up. If the same resource keeps failing, 'cdkd state orphan ${stackName}' is the last resort: it removes the state record without deleting AWS resources.`);
|
|
52534
52608
|
} finally {
|
|
52535
52609
|
process.removeListener("SIGINT", sigintHandler);
|
|
52536
52610
|
renderer.stop();
|
|
@@ -52749,7 +52823,7 @@ var NestedStackProvider = class {
|
|
|
52749
52823
|
}
|
|
52750
52824
|
const resourceCount = Object.keys(childStateData.state.resources).length;
|
|
52751
52825
|
this.logger.info(`Destroying nested stack ${childStackName} (logicalId=${logicalId}, ${resourceCount} resource(s))`);
|
|
52752
|
-
await withNestedStackContext({
|
|
52826
|
+
const childResult = await withNestedStackContext({
|
|
52753
52827
|
...ctx,
|
|
52754
52828
|
parentStackName: childStackName,
|
|
52755
52829
|
parentRegion: childRegion,
|
|
@@ -52771,6 +52845,15 @@ var NestedStackProvider = class {
|
|
|
52771
52845
|
...ctx.destroyOptions?.resourceWarnAfterByType && { resourceWarnAfterByType: ctx.destroyOptions.resourceWarnAfterByType },
|
|
52772
52846
|
...ctx.destroyOptions?.resourceTimeoutByType && { resourceTimeoutByType: ctx.destroyOptions.resourceTimeoutByType }
|
|
52773
52847
|
}));
|
|
52848
|
+
if (childResult.skippedCount > 0 || childResult.interrupted) {
|
|
52849
|
+
const causes = [];
|
|
52850
|
+
if (childResult.skippedCount > 0) causes.push(`skipped ${childResult.skippedCount} resource(s)`);
|
|
52851
|
+
if (childResult.interrupted) causes.push("was interrupted");
|
|
52852
|
+
return {
|
|
52853
|
+
outcome: "skipped",
|
|
52854
|
+
reason: `nested stack ${childStackName} ${causes.join(" and ")}`
|
|
52855
|
+
};
|
|
52856
|
+
}
|
|
52774
52857
|
}
|
|
52775
52858
|
async getAttribute(_physicalId, _resourceType, attributeName) {
|
|
52776
52859
|
throw new Error(`AWS::CloudFormation::Stack: attribute '${attributeName}' is not in the recorded Outputs map. Only 'Outputs.<Key>' references to declared Output names on the child template are supported.`);
|
|
@@ -57689,6 +57772,7 @@ async function destroyCommand(stackArgs, options) {
|
|
|
57689
57772
|
} else throw new Error("Could not determine which stacks belong to this app. Specify stack names explicitly, use --all, or ensure --app / cdk.json is configured.");
|
|
57690
57773
|
const stackPatterns = stackArgs.length > 0 ? stackArgs : options.stack ? [options.stack] : [];
|
|
57691
57774
|
let totalErrors = 0;
|
|
57775
|
+
let totalSkipped = 0;
|
|
57692
57776
|
let interrupted = false;
|
|
57693
57777
|
let stackNames;
|
|
57694
57778
|
if (options.all) stackNames = candidateStacks.map((s) => s.stackName);
|
|
@@ -57813,8 +57897,9 @@ async function destroyCommand(stackArgs, options) {
|
|
|
57813
57897
|
eventRecorder
|
|
57814
57898
|
}));
|
|
57815
57899
|
totalErrors += result.errorCount;
|
|
57900
|
+
totalSkipped += result.skippedCount;
|
|
57816
57901
|
if (result.interrupted) interrupted = true;
|
|
57817
|
-
destroyRunResult = result.errorCount > 0 ? "FAILED" : "SUCCEEDED";
|
|
57902
|
+
destroyRunResult = result.errorCount > 0 || result.skippedCount > 0 ? "FAILED" : "SUCCEEDED";
|
|
57818
57903
|
eventRecorder.record({
|
|
57819
57904
|
eventType: "RUN_FINISHED",
|
|
57820
57905
|
stackName,
|
|
@@ -57823,7 +57908,8 @@ async function destroyCommand(stackArgs, options) {
|
|
|
57823
57908
|
created: 0,
|
|
57824
57909
|
updated: 0,
|
|
57825
57910
|
deleted: result.deletedCount,
|
|
57826
|
-
...result.errorCount > 0 && { failed: result.errorCount }
|
|
57911
|
+
...result.errorCount > 0 && { failed: result.errorCount },
|
|
57912
|
+
...result.skippedCount > 0 && { skipped: result.skippedCount }
|
|
57827
57913
|
}
|
|
57828
57914
|
});
|
|
57829
57915
|
} catch (destroyError) {
|
|
@@ -57842,6 +57928,7 @@ async function destroyCommand(stackArgs, options) {
|
|
|
57842
57928
|
}
|
|
57843
57929
|
if (totalErrors > 0) throw new PartialFailureError(`Destroy completed with ${totalErrors} resource error(s). State preserved — inspect 'cdkd state show <stack>' and re-run 'cdkd destroy' to retry. If the same resource keeps failing, 'cdkd state orphan <stack>' removes the state record without deleting AWS resources.`);
|
|
57844
57930
|
if (interrupted) throw new PartialFailureError(`Destroy interrupted by Ctrl-C. State preserved — re-run 'cdkd destroy' to finish.`);
|
|
57931
|
+
if (totalSkipped > 0) throw new PartialFailureError(`Destroy skipped ${totalSkipped} entr${totalSkipped === 1 ? "y" : "ies"} cdkd could not address, so the underlying resources may still exist in AWS. A skipped nested stack counts as ONE entry and may cover several of its own resources — the per-stack summaries above give the exact breakdown. State preserved (the records are kept). Repair the physicalId in state.json and re-run 'cdkd destroy', or delete the resources by hand and drop the records with 'cdkd state orphan <stack>'.`);
|
|
57845
57932
|
} finally {
|
|
57846
57933
|
unforwardSigterm();
|
|
57847
57934
|
awsClients.destroy();
|
|
@@ -58251,9 +58338,17 @@ function stackTreeToJson(roots) {
|
|
|
58251
58338
|
* PR B2 — until that lands, the orchestrator hard-errors at submission
|
|
58252
58339
|
* time with a clear pointer.
|
|
58253
58340
|
*
|
|
58254
|
-
* The list is intentionally narrow
|
|
58255
|
-
*
|
|
58256
|
-
*
|
|
58341
|
+
* The list is intentionally narrow, and stays hand-written because each member
|
|
58342
|
+
* is a cdkd / CDK construct rather than something the registry describes.
|
|
58343
|
+
*
|
|
58344
|
+
* It is NOT the only refusal, though — the "defer to the CreateChangeSet
|
|
58345
|
+
* error" note this comment used to end on was revisited in issue
|
|
58346
|
+
* [#1659](https://github.com/go-to-k/cdkd/issues/1659). Types AWS refuses for
|
|
58347
|
+
* IMPORT because their registry schema declares no `read` handler are now
|
|
58348
|
+
* caught by {@link buildImportPlan}'s pre-flight instead, which needs no
|
|
58349
|
+
* hand-maintained list (the signal is derived from the schema cdkd already
|
|
58350
|
+
* fetches for the identifier) and names every offender in one pass — AWS's own
|
|
58351
|
+
* error names only some of them.
|
|
58257
58352
|
*/
|
|
58258
58353
|
const NEVER_IMPORTABLE_TYPES = /* @__PURE__ */ new Set(["AWS::CDK::Metadata"]);
|
|
58259
58354
|
/**
|
|
@@ -58426,6 +58521,16 @@ const COMPOSITE_ID_SPLITTERS = {
|
|
|
58426
58521
|
propertiesOverlay: { ApiId: apiId }
|
|
58427
58522
|
};
|
|
58428
58523
|
},
|
|
58524
|
+
"AWS::S3Tables::Namespace": (physicalId) => {
|
|
58525
|
+
const parts = physicalId.split("|");
|
|
58526
|
+
if (parts.length !== 2) throw new Error(`expected 2 parts ('<tableBucketARN>|<namespaceName>'), got ${parts.length}: '${physicalId}'`);
|
|
58527
|
+
const [tableBucketARN, namespace] = parts;
|
|
58528
|
+
if (!tableBucketARN || !namespace) throw new Error(`empty part in '<tableBucketARN>|<namespaceName>': '${physicalId}'`);
|
|
58529
|
+
return { resourceIdentifier: {
|
|
58530
|
+
TableBucketARN: tableBucketARN,
|
|
58531
|
+
Namespace: namespace
|
|
58532
|
+
} };
|
|
58533
|
+
},
|
|
58429
58534
|
"AWS::Lambda::Permission": (physicalId, properties) => {
|
|
58430
58535
|
const functionName = readStringProperty(properties, "FunctionName", "AWS::Lambda::Permission");
|
|
58431
58536
|
return {
|
|
@@ -58511,6 +58616,144 @@ function readStringProperty(properties, key, resourceType) {
|
|
|
58511
58616
|
return v;
|
|
58512
58617
|
}
|
|
58513
58618
|
/**
|
|
58619
|
+
* Types whose cdkd physicalId is a COMPOSITE (`|`-joined) string while
|
|
58620
|
+
* CloudFormation's `primaryIdentifier` is a SINGLE field — so the physicalId is
|
|
58621
|
+
* NOT the identifier value, and it is not a SPLIT of the composite either
|
|
58622
|
+
* (issue [#1659](https://github.com/go-to-k/cdkd/issues/1659)).
|
|
58623
|
+
*
|
|
58624
|
+
* ## Why this table exists next to `COMPOSITE_ID_SPLITTERS`
|
|
58625
|
+
*
|
|
58626
|
+
* `resolveResourceIdentifier` used to conflate two independent questions: the
|
|
58627
|
+
* ARITY of the CFn identifier, and the FORMAT of cdkd's physical id. The
|
|
58628
|
+
* single-field branch assumed "the physicalId IS the identifier value", which
|
|
58629
|
+
* holds for every type whose id is a scalar and silently produces a wrong value
|
|
58630
|
+
* for a composite-id type whose CFn identifier happens to be single-field —
|
|
58631
|
+
* `{ TableARN: 'arn:aws:s3tables:...|analytics|events' }` for an
|
|
58632
|
+
* `AWS::S3Tables::Table`. `COMPOSITE_ID_SPLITTERS` cannot fix that: it is only
|
|
58633
|
+
* consulted on the multi-field branch, so registering a type there would add an
|
|
58634
|
+
* entry no code path reaches.
|
|
58635
|
+
*
|
|
58636
|
+
* This table is consulted BEFORE the arity branch, so the two concerns are
|
|
58637
|
+
* separated: a registered type never takes the "physicalId is the identifier"
|
|
58638
|
+
* assumption regardless of what the schema's field count says.
|
|
58639
|
+
*
|
|
58640
|
+
* ## Why a RESOLVE and not a split
|
|
58641
|
+
*
|
|
58642
|
+
* The correct identifier for each member is a different value entirely, not a
|
|
58643
|
+
* segment of the composite — an ARN AWS minted, or an `sgr-` rule id. So each
|
|
58644
|
+
* entry reads the value cdkd recorded in state (`attributes`), and refuses when
|
|
58645
|
+
* it is absent rather than guessing.
|
|
58646
|
+
*
|
|
58647
|
+
* ## Live measurement (us-east-1, 2026-08-13)
|
|
58648
|
+
*
|
|
58649
|
+
* Every field below is BOTH the type's single-field `primaryIdentifier` AND
|
|
58650
|
+
* `readOnlyProperties`, which is why each resolution returns an EMPTY
|
|
58651
|
+
* `propertiesOverlay`: writing a read-only property into the synth template's
|
|
58652
|
+
* `Properties` block is rejected by CFn at changeset-create.
|
|
58653
|
+
*
|
|
58654
|
+
* | Type | `primaryIdentifier` | recorded as |
|
|
58655
|
+
* |---|---|---|
|
|
58656
|
+
* | `AWS::AppSync::DataSource` | `DataSourceArn` | `attributes.DataSourceArn` |
|
|
58657
|
+
* | `AWS::AppSync::Resolver` | `ResolverArn` | `attributes.ResolverArn` |
|
|
58658
|
+
* | `AWS::S3Tables::Table` | `TableARN` | `attributes.TableARN` |
|
|
58659
|
+
* | `AWS::EC2::SecurityGroupIngress` | `Id` | NOTHING (issue #1761) |
|
|
58660
|
+
*
|
|
58661
|
+
* All four declare a `read` handler, so CFn genuinely does accept them for
|
|
58662
|
+
* IMPORT — unlike `AWS::Glue::Table`, the type issue #1659's title names, which
|
|
58663
|
+
* CFn rejects outright (see {@link typeIsImportUnsupported}).
|
|
58664
|
+
*/
|
|
58665
|
+
const COMPOSITE_PHYSICAL_ID_IDENTIFIERS = {
|
|
58666
|
+
"AWS::AppSync::DataSource": recordedArnIdentifier({
|
|
58667
|
+
resourceType: "AWS::AppSync::DataSource",
|
|
58668
|
+
field: "DataSourceArn",
|
|
58669
|
+
physicalIdShape: "<apiId>|<name>",
|
|
58670
|
+
arnServiceSegment: "appsync"
|
|
58671
|
+
}),
|
|
58672
|
+
"AWS::AppSync::Resolver": recordedArnIdentifier({
|
|
58673
|
+
resourceType: "AWS::AppSync::Resolver",
|
|
58674
|
+
field: "ResolverArn",
|
|
58675
|
+
physicalIdShape: "<apiId>|<typeName>|<fieldName>",
|
|
58676
|
+
arnServiceSegment: "appsync"
|
|
58677
|
+
}),
|
|
58678
|
+
"AWS::S3Tables::Table": recordedArnIdentifier({
|
|
58679
|
+
resourceType: "AWS::S3Tables::Table",
|
|
58680
|
+
field: "TableARN",
|
|
58681
|
+
physicalIdShape: "<tableBucketARN>|<namespace>|<name>",
|
|
58682
|
+
arnServiceSegment: "s3tables"
|
|
58683
|
+
}),
|
|
58684
|
+
"AWS::EC2::SecurityGroupIngress": {
|
|
58685
|
+
field: "Id",
|
|
58686
|
+
resolve: ({ logicalId }) => {
|
|
58687
|
+
throw new Error(`cdkd's physical id for AWS::EC2::SecurityGroupIngress is the composite '<groupId>|<ipProtocol>|<fromPort>|<toPort>', but CloudFormation IMPORT identifies the rule by the 'sgr-...' security-group rule id AWS assigns, which cdkd does not record in state (tracked in https://github.com/go-to-k/cdkd/issues/1761). Remove '${logicalId}' from the stack before exporting (the rule stays in AWS and can be re-declared in CloudFormation afterwards), or destroy it first and let CloudFormation create it fresh.`);
|
|
58688
|
+
}
|
|
58689
|
+
}
|
|
58690
|
+
};
|
|
58691
|
+
/**
|
|
58692
|
+
* Build the {@link CompositePhysicalIdIdentifier} for a type whose CFn
|
|
58693
|
+
* identifier is an ARN cdkd records in `attributes` under the SAME name as the
|
|
58694
|
+
* `primaryIdentifier` field.
|
|
58695
|
+
*
|
|
58696
|
+
* Two accepted sources, mirroring the splitter family's "both shapes exist in
|
|
58697
|
+
* state in the wild" contract:
|
|
58698
|
+
*
|
|
58699
|
+
* 1. the recorded attribute, which is what a fresh deploy and a cdkd `import`
|
|
58700
|
+
* both write; and
|
|
58701
|
+
* 2. a physicalId that is ALREADY the bare ARN. Two live producers:
|
|
58702
|
+
* `S3TablesProvider.importTable` records it for a Cloud-Control-routed
|
|
58703
|
+
* table (CC's identifier for the type IS the ARN), and an AppSync child
|
|
58704
|
+
* adopted through `cdkd import --migrate-from-cloudformation` records
|
|
58705
|
+
* CloudFormation's `PhysicalResourceId` verbatim, which is likewise the
|
|
58706
|
+
* ARN.
|
|
58707
|
+
*
|
|
58708
|
+
* **Both sources are validated by the SAME predicate.** An earlier cut checked
|
|
58709
|
+
* the ARN's shape only on source 2 — the rare one — while source 1, which
|
|
58710
|
+
* fires on essentially every export, accepted any non-blank string verbatim.
|
|
58711
|
+
* That is the wrong way round: a hand-edited state row, a row written by an
|
|
58712
|
+
* older binary that recorded the composite under the attribute name, or an ARN
|
|
58713
|
+
* for a different service sitting under `attributes.<field>` would each have
|
|
58714
|
+
* been shipped as the IMPORT identifier and silently adopted the WRONG
|
|
58715
|
+
* resource, which is precisely what this table exists to prevent.
|
|
58716
|
+
*
|
|
58717
|
+
* Anything failing both sources is refused. That covers the degraded record
|
|
58718
|
+
* `docs/state-management.md` describes — written by a cdkd older than the fix
|
|
58719
|
+
* that started recording the ARN, or by an import that could not reach STS —
|
|
58720
|
+
* and the remedy is the one that doc already gives.
|
|
58721
|
+
*/
|
|
58722
|
+
function recordedArnIdentifier(options) {
|
|
58723
|
+
const { resourceType, field, physicalIdShape, arnServiceSegment } = options;
|
|
58724
|
+
/**
|
|
58725
|
+
* Is this value usable AS the type's CFn identifier?
|
|
58726
|
+
*
|
|
58727
|
+
* - `arn:` prefix and the type's own SERVICE segment: an ARN naming another
|
|
58728
|
+
* service is a different value entirely. Partition-agnostic by
|
|
58729
|
+
* construction, so `arn:aws-cn:s3tables:...` matches.
|
|
58730
|
+
* - no `|`: cdkd's composite for `AWS::S3Tables::Table` is built FROM the
|
|
58731
|
+
* bucket ARN, so it satisfies both checks above — the separator test is
|
|
58732
|
+
* what distinguishes the composite from the identifier, on BOTH sources.
|
|
58733
|
+
*/
|
|
58734
|
+
const isUsableIdentifier = (value) => {
|
|
58735
|
+
const trimmed = value.trim();
|
|
58736
|
+
return trimmed.startsWith("arn:") && trimmed.includes(`:${arnServiceSegment}:`) && !trimmed.includes("|");
|
|
58737
|
+
};
|
|
58738
|
+
return {
|
|
58739
|
+
field,
|
|
58740
|
+
resolve: ({ logicalId, physicalId, attributes }) => {
|
|
58741
|
+
const recorded = attributes[field];
|
|
58742
|
+
if (typeof recorded === "string" && isUsableIdentifier(recorded)) return recorded.trim();
|
|
58743
|
+
if (isUsableIdentifier(physicalId)) return physicalId.trim();
|
|
58744
|
+
const recordedNote = typeof recorded === "string" && recorded.trim() ? `attributes.${field} is recorded as '${recorded.trim()}', which is not a ${arnServiceSegment} ARN` : `attributes.${field} is missing or empty`;
|
|
58745
|
+
throw new Error(`cdkd's physical id for ${resourceType} is the composite '${physicalIdShape}', but CloudFormation IMPORT identifies the resource by its ${field}, which cdkd state does not record usably for '${logicalId}' (${recordedNote}). Re-deploy the stack once so cdkd records ${field}, then re-run cdkd export.`);
|
|
58746
|
+
}
|
|
58747
|
+
};
|
|
58748
|
+
}
|
|
58749
|
+
/**
|
|
58750
|
+
* Every type registered in {@link COMPOSITE_PHYSICAL_ID_IDENTIFIERS}. Exported
|
|
58751
|
+
* so a test can assert the table's SIZE, not just membership — a new entry
|
|
58752
|
+
* needs a measurement row in the table's doc comment, and a size-blind test
|
|
58753
|
+
* would let one land silently.
|
|
58754
|
+
*/
|
|
58755
|
+
const COMPOSITE_PHYSICAL_ID_IDENTIFIER_TYPES = Object.keys(COMPOSITE_PHYSICAL_ID_IDENTIFIERS);
|
|
58756
|
+
/**
|
|
58514
58757
|
* Resource types AWS does NOT support in IMPORT changesets but DOES
|
|
58515
58758
|
* support normal CREATE for. cdkd handles these via a pre-delete + phase-2
|
|
58516
58759
|
* CREATE dance (see {@link RecreateBeforePhase2Entry}). When the user
|
|
@@ -58664,17 +58907,20 @@ async function exportCommand(stackArg, options) {
|
|
|
58664
58907
|
}
|
|
58665
58908
|
}
|
|
58666
58909
|
}
|
|
58910
|
+
const { phase1Imports, phase2Creates, recreateBeforePhase2, nestedStackRows, blocked } = await buildImportPlan(state, template, awsClients.cloudFormation, resolvedStackName, {
|
|
58911
|
+
recreateImportUnsupported: options.recreateImportUnsupported,
|
|
58912
|
+
skipImportSupportPreflight: options.skipImportSupportPreflight
|
|
58913
|
+
});
|
|
58914
|
+
if (blocked.length > 0) {
|
|
58915
|
+
logger.error("The following resources block migration:");
|
|
58916
|
+
for (const b of blocked) logger.error(` - ${b.logicalId} (${b.resourceType}): ${b.reason}`);
|
|
58917
|
+
throw new Error(`${blocked.length} resource(s) block migration. Either destroy them first (cdkd destroy / cdkd state destroy cherry-picked), or remove them from the CDK app and re-synthesize.`);
|
|
58918
|
+
}
|
|
58667
58919
|
const owner = `${process.env["USER"] || "unknown"}@${process.env["HOSTNAME"] || "host"}:${process.pid}`;
|
|
58668
58920
|
if (!options.dryRun) {
|
|
58669
58921
|
if (!await lockManager.acquireLock(resolvedStackName, targetRegion, owner, "export")) throw new Error(`Could not acquire lock for stack '${resolvedStackName}' (${targetRegion}) — another cdkd process holds it. Wait for it to finish, or run 'cdkd force-unlock ${resolvedStackName}' if you are certain no other process is active.`);
|
|
58670
58922
|
}
|
|
58671
58923
|
try {
|
|
58672
|
-
const { phase1Imports, phase2Creates, recreateBeforePhase2, nestedStackRows, blocked } = await buildImportPlan(state, template, awsClients.cloudFormation, resolvedStackName, { recreateImportUnsupported: options.recreateImportUnsupported });
|
|
58673
|
-
if (blocked.length > 0) {
|
|
58674
|
-
logger.error("The following resources block migration:");
|
|
58675
|
-
for (const b of blocked) logger.error(` - ${b.logicalId} (${b.resourceType}): ${b.reason}`);
|
|
58676
|
-
throw new Error(`${blocked.length} resource(s) block migration. Either destroy them first (cdkd destroy / cdkd state destroy cherry-picked), or remove them from the CDK app and re-synthesize.`);
|
|
58677
|
-
}
|
|
58678
58924
|
if (nestedStackRows.length > 0) {
|
|
58679
58925
|
const nestedStackTree = await buildCdkdStateStackTree(resolvedStackName, targetRegion, stateBackend, state);
|
|
58680
58926
|
reportDriftBaselineGaps(state, logger);
|
|
@@ -58717,7 +58963,8 @@ async function exportCommand(stackArg, options) {
|
|
|
58717
58963
|
dryRun: options.dryRun,
|
|
58718
58964
|
yes: options.yes,
|
|
58719
58965
|
includeNonImportable: options.includeNonImportable,
|
|
58720
|
-
recreateImportUnsupported: options.recreateImportUnsupported
|
|
58966
|
+
recreateImportUnsupported: options.recreateImportUnsupported,
|
|
58967
|
+
skipImportSupportPreflight: options.skipImportSupportPreflight
|
|
58721
58968
|
}
|
|
58722
58969
|
})).outcome === "success") printNextSteps({
|
|
58723
58970
|
cfnStackName: options.cfnStackName ?? cdkd2cfnStackName(resolvedStackName),
|
|
@@ -59256,7 +59503,16 @@ async function buildImportPlan(state, template, cfnClient, parentStackName, opti
|
|
|
59256
59503
|
}
|
|
59257
59504
|
let resolved;
|
|
59258
59505
|
try {
|
|
59259
|
-
|
|
59506
|
+
const schemaInfo = await cachedTypeSchemaInfo(resourceType, cfnClient, identifierCache);
|
|
59507
|
+
if (!options.skipImportSupportPreflight && typeIsImportUnsupported(schemaInfo)) {
|
|
59508
|
+
blocked.push({
|
|
59509
|
+
logicalId,
|
|
59510
|
+
resourceType,
|
|
59511
|
+
reason: `AWS CloudFormation does not support ${resourceType} in IMPORT changesets: its CloudFormation registry schema declares no 'read' handler (and reports the type as NON_PROVISIONABLE), and IMPORT needs that handler to look the resource up by identifier. CreateChangeSet would reject the whole export with "ResourceTypes [${resourceType}] are not supported for Import". Remove the resource from the stack before exporting (it stays in AWS and can be re-declared in CloudFormation afterwards), or destroy it first and let CloudFormation create it fresh. If AWS has since added IMPORT support for this type, please open a cdkd issue.`
|
|
59512
|
+
});
|
|
59513
|
+
continue;
|
|
59514
|
+
}
|
|
59515
|
+
resolved = resolveResourceIdentifier(resourceType, logicalId, stateEntry.physicalId, stateEntry.properties ?? {}, stateEntry.attributes ?? {}, schemaInfo);
|
|
59260
59516
|
} catch (err) {
|
|
59261
59517
|
blocked.push({
|
|
59262
59518
|
logicalId,
|
|
@@ -59286,6 +59542,11 @@ async function buildImportPlan(state, template, cfnClient, parentStackName, opti
|
|
|
59286
59542
|
* `ResourcesToImport[].ResourceIdentifier` for the given resource type
|
|
59287
59543
|
* and cdkd state's physical ID.
|
|
59288
59544
|
*
|
|
59545
|
+
* Types whose cdkd physicalId is COMPOSITE while the CFn identifier is a
|
|
59546
|
+
* single field (`COMPOSITE_PHYSICAL_ID_IDENTIFIERS`): resolved from cdkd
|
|
59547
|
+
* state's recorded attributes, BEFORE the field-count branch below — the
|
|
59548
|
+
* physicalId is neither the identifier nor a source to split (issue #1659).
|
|
59549
|
+
*
|
|
59289
59550
|
* Single-key types: the map has a single entry keyed by the schema's
|
|
59290
59551
|
* primaryIdentifier field name (e.g. `{ BucketName: 'my-bucket' }`).
|
|
59291
59552
|
*
|
|
@@ -59295,16 +59556,27 @@ async function buildImportPlan(state, template, cfnClient, parentStackName, opti
|
|
|
59295
59556
|
* into one entry per field. Composite types without a registered
|
|
59296
59557
|
* splitter surface a clear error pointing at where to add one.
|
|
59297
59558
|
*
|
|
59298
|
-
*
|
|
59299
|
-
*
|
|
59300
|
-
*
|
|
59301
|
-
*
|
|
59302
|
-
|
|
59303
|
-
|
|
59304
|
-
|
|
59305
|
-
|
|
59306
|
-
|
|
59307
|
-
|
|
59559
|
+
* `entry` is the caller's already-resolved registry-schema info — see
|
|
59560
|
+
* {@link cachedTypeSchemaInfo}, which prefers `DescribeType` (the authoritative
|
|
59561
|
+
* AWS-internal registry) and falls back to a hardcoded single-key table when
|
|
59562
|
+
* DescribeType fails (insufficient permissions, throttling, obscure type
|
|
59563
|
+
* without a registry entry). Taking it as a parameter rather than fetching is
|
|
59564
|
+
* what keeps `buildImportPlan` to ONE DescribeType per type across both the
|
|
59565
|
+
* IMPORT pre-flight and this resolution.
|
|
59566
|
+
*/
|
|
59567
|
+
function resolveResourceIdentifier(resourceType, logicalId, physicalId, properties, attributes, entry) {
|
|
59568
|
+
const compositeIdentifier = COMPOSITE_PHYSICAL_ID_IDENTIFIERS[resourceType];
|
|
59569
|
+
if (compositeIdentifier) {
|
|
59570
|
+
if (entry.fields.length !== 1 || entry.fields[0] !== compositeIdentifier.field) throw new Error(`${resourceType} is registered in COMPOSITE_PHYSICAL_ID_IDENTIFIERS as identified by '${compositeIdentifier.field}', but CloudFormation now reports primaryIdentifier [${entry.fields.join(", ")}]. The registry schema changed under cdkd — update the entry in src/cli/commands/export.ts (or move the type to COMPOSITE_ID_SPLITTERS if its identifier is now composite too)`);
|
|
59571
|
+
const value = compositeIdentifier.resolve({
|
|
59572
|
+
logicalId,
|
|
59573
|
+
physicalId,
|
|
59574
|
+
attributes
|
|
59575
|
+
});
|
|
59576
|
+
return {
|
|
59577
|
+
resourceIdentifier: { [compositeIdentifier.field]: value },
|
|
59578
|
+
propertiesOverlay: {}
|
|
59579
|
+
};
|
|
59308
59580
|
}
|
|
59309
59581
|
if (entry.fields.length === 1) return { resourceIdentifier: { [entry.fields[0]]: physicalId } };
|
|
59310
59582
|
const splitter = COMPOSITE_ID_SPLITTERS[resourceType];
|
|
@@ -59319,6 +59591,53 @@ async function resolveResourceIdentifier(resourceType, physicalId, properties, c
|
|
|
59319
59591
|
return result;
|
|
59320
59592
|
}
|
|
59321
59593
|
/**
|
|
59594
|
+
* Cache-consulting wrapper around {@link fetchPrimaryIdentifier}. Shared by
|
|
59595
|
+
* `resolveResourceIdentifier` and `buildImportPlan`'s pre-flight so one
|
|
59596
|
+
* DescribeType per type answers both questions.
|
|
59597
|
+
*/
|
|
59598
|
+
async function cachedTypeSchemaInfo(resourceType, cfnClient, cache) {
|
|
59599
|
+
const cached = cache.get(resourceType);
|
|
59600
|
+
if (cached !== void 0) return cached;
|
|
59601
|
+
const entry = await fetchPrimaryIdentifier(resourceType, cfnClient);
|
|
59602
|
+
cache.set(resourceType, entry);
|
|
59603
|
+
return entry;
|
|
59604
|
+
}
|
|
59605
|
+
/**
|
|
59606
|
+
* True when the registry schema says CloudFormation will refuse the type in an
|
|
59607
|
+
* IMPORT changeset (`ValidationError: ResourceTypes [<T>] are not supported
|
|
59608
|
+
* for Import`) — IMPORT looks the resource up by identifier through the type's
|
|
59609
|
+
* `read` handler, so a type without one cannot be adopted.
|
|
59610
|
+
*
|
|
59611
|
+
* Live-measured (us-east-1, 2026-08-13) against `CreateChangeSet
|
|
59612
|
+
* --change-set-type IMPORT`: `AWS::Glue::Table` / `AWS::Route53::RecordSet` /
|
|
59613
|
+
* `AWS::AppSync::ApiKey` / `AWS::EC2::NetworkAclEntry` /
|
|
59614
|
+
* `AWS::Route53::RecordSetGroup` / `AWS::SQS::QueuePolicy` /
|
|
59615
|
+
* `AWS::SNS::TopicPolicy` all lack a `read` handler, are all
|
|
59616
|
+
* `NON_PROVISIONABLE`, and were all rejected; `AWS::S3::Bucket`
|
|
59617
|
+
* (`FULLY_MUTABLE`, read handler present) passed the type check. Both shapes
|
|
59618
|
+
* of "no read handler" occur and both count: NO `handlers` block at all (the
|
|
59619
|
+
* legacy types) and a `handlers` block whose keys are only create / update /
|
|
59620
|
+
* delete (`AWS::EC2::NetworkAclEntry`).
|
|
59621
|
+
*
|
|
59622
|
+
* **Why the verdict needs TWO agreeing fields.** Reading "the `handlers` key is
|
|
59623
|
+
* missing" as a refusal on its own makes any partial or unusual response block
|
|
59624
|
+
* an export that works today — the one regression this pre-flight could cause,
|
|
59625
|
+
* and strictly worse than the status quo of letting AWS answer. So the refusal
|
|
59626
|
+
* also requires `ProvisioningType: NON_PROVISIONABLE`; a response satisfying
|
|
59627
|
+
* only one of the two resolves to `unknown` and is left to CreateChangeSet.
|
|
59628
|
+
*
|
|
59629
|
+
* The two were perfectly CORRELATED across the 21 types sampled on 2026-08-13
|
|
59630
|
+
* (every no-read-handler type was NON_PROVISIONABLE and vice versa), so on
|
|
59631
|
+
* today's registry the conjunction's real work is requiring a COMPLETE
|
|
59632
|
+
* DescribeType response before refusing. The `read` handler stays the primary
|
|
59633
|
+
* signal because it is the mechanistic reason CFn accepts a type for IMPORT —
|
|
59634
|
+
* refusing on `ProvisioningType` alone would reject a NON_PROVISIONABLE type
|
|
59635
|
+
* that does declare one, should AWS ever publish that combination.
|
|
59636
|
+
*/
|
|
59637
|
+
function typeIsImportUnsupported(entry) {
|
|
59638
|
+
return entry.importSupport === "unsupported";
|
|
59639
|
+
}
|
|
59640
|
+
/**
|
|
59322
59641
|
* Fetch the primary identifier field names for a resource type, with a
|
|
59323
59642
|
* hardcoded single-key fallback when DescribeType is unavailable.
|
|
59324
59643
|
*/
|
|
@@ -59326,15 +59645,26 @@ async function fetchPrimaryIdentifier(resourceType, cfnClient) {
|
|
|
59326
59645
|
try {
|
|
59327
59646
|
const resp = await describeTypeWithThrottleRetry(resourceType, cfnClient);
|
|
59328
59647
|
if (resp.Schema) {
|
|
59329
|
-
const
|
|
59330
|
-
|
|
59648
|
+
const parsed = JSON.parse(resp.Schema);
|
|
59649
|
+
const primary = parsed.primaryIdentifier;
|
|
59650
|
+
if (Array.isArray(primary) && primary.length > 0 && primary.every((p) => typeof p === "string")) {
|
|
59651
|
+
const fields = primary.map((p) => p.replace(/^\/properties\//, ""));
|
|
59652
|
+
const handlers = parsed.handlers;
|
|
59653
|
+
return {
|
|
59654
|
+
fields,
|
|
59655
|
+
importSupport: typeof handlers === "object" && handlers !== null && Object.prototype.hasOwnProperty.call(handlers, "read") ? "importable" : resp.ProvisioningType === "NON_PROVISIONABLE" ? "unsupported" : "unknown"
|
|
59656
|
+
};
|
|
59657
|
+
}
|
|
59331
59658
|
}
|
|
59332
59659
|
} catch (err) {
|
|
59333
59660
|
const msg = err instanceof Error ? err.message : String(err);
|
|
59334
59661
|
getLogger().debug(`DescribeType failed for ${resourceType}: ${msg} — using fallback`);
|
|
59335
59662
|
}
|
|
59336
59663
|
const fallback = PRIMARY_IDENTIFIER_FALLBACK[resourceType];
|
|
59337
|
-
if (fallback) return {
|
|
59664
|
+
if (fallback) return {
|
|
59665
|
+
fields: [fallback],
|
|
59666
|
+
importSupport: "unknown"
|
|
59667
|
+
};
|
|
59338
59668
|
throw new Error(`primary identifier unknown (DescribeType returned no usable schema and no fallback is registered). Add ${resourceType} to PRIMARY_IDENTIFIER_FALLBACK in export.ts, or open an issue.`);
|
|
59339
59669
|
}
|
|
59340
59670
|
/**
|
|
@@ -60260,7 +60590,10 @@ async function runPerStackImportLoop(args) {
|
|
|
60260
60590
|
const perStackPlans = [];
|
|
60261
60591
|
for (const node of leafFirst) {
|
|
60262
60592
|
const meta = nodesByCdkdName.get(node.stackName);
|
|
60263
|
-
const plan = await buildImportPlan(meta.state, meta.template, deps.cfnClient, meta.cdkdStackName, {
|
|
60593
|
+
const plan = await buildImportPlan(meta.state, meta.template, deps.cfnClient, meta.cdkdStackName, {
|
|
60594
|
+
recreateImportUnsupported: options.recreateImportUnsupported,
|
|
60595
|
+
skipImportSupportPreflight: options.skipImportSupportPreflight === true
|
|
60596
|
+
});
|
|
60264
60597
|
if (plan.blocked.length > 0) {
|
|
60265
60598
|
const lines = plan.blocked.map((b) => ` - ${b.logicalId} (${b.resourceType}): ${b.reason}`);
|
|
60266
60599
|
throw new Error(`Stack '${meta.cdkdStackName}' has ${plan.blocked.length} resource(s) that block migration:\n${lines.join("\n")}`);
|
|
@@ -60676,7 +61009,7 @@ async function confirmPrompt$3(prompt) {
|
|
|
60676
61009
|
}
|
|
60677
61010
|
}
|
|
60678
61011
|
function createExportCommand() {
|
|
60679
|
-
const cmd = new Command("export").description("Hand a cdkd-managed stack over to CloudFormation via CFn IMPORT (changeset). AWS resources are unchanged; cdkd state for the stack is deleted on success. Mirror of `cdkd import` (AWS → cdkd) in the reverse direction (cdkd → CFn). Accepts JSON and YAML templates (YAML via a CFn-aware codec that preserves !Ref / !GetAtt / !Sub shorthand). Aborts if any resource is not CFn-importable.").argument("[stack]", "Stack name to export (auto-detected for single-stack apps)").option("--cfn-stack-name <name>", "Name of the destination CloudFormation stack for the root stack. Defaults to the cdkd stack name (or the cdkd2cfnStackName mapping when the name contains '~'). For per-nested-child overrides, use --cfn-child-stack-name.").option("--cfn-child-stack-name <pair...>", "Per-nested-child CFn stack-name override. Repeatable. Format: '<cdkdName>=<cfnName>' (e.g. --cfn-child-stack-name 'MyApp~Database=my-app-db'). The cdkd stack name for a nested child is '<parent>~<childLogicalId>' (v6 state-key form). Without an override the CFn name is derived by replacing '~' with '-'. Only consulted when the exported stack tree contains nested children; ignored for flat stacks.").option("--template <path>", "Path to a pre-rendered CloudFormation template (JSON or YAML — format auto-detected). Skips synth.").option("--stack-region <region>", "Region of the cdkd state record to operate on. Required when the same stack name has state in multiple regions.").option("--dry-run", "Print the import plan without creating a changeset.", false).option("--accept-transient-context", "Allow CLI -c key=value overrides at export time even though they are not persisted to cdk.json / cdk.context.json (default: refuse). When set, the user is responsible for passing the same -c flags to every future cdk deploy.", false).option("--include-non-importable", "Run a 2-phase migration when the stack contains non-importable resources (Custom::*). Phase 1 imports the importable resources; phase 2 CFn-CREATEs the non-importable ones, which re-invokes each Custom Resource's onCreate handler. Make sure onCreate is idempotent before enabling.", false).option("--parameter <key=value...>", "CFn template Parameter override, repeatable. Required when the synthesized template has Parameters without Default values; otherwise overrides the template's default value. Format: --parameter Key=Value.").option("--strict-cross-stack", "Refuse to export when sibling cdkd stacks in the same CDK app reference the exporting stack via Fn::GetStackOutput. Without the flag, cdkd warns and proceeds — the consumers keep resolving via the CloudFormation fallback (weak reference) unless they deploy with --no-cfn-fallback.", false).option("--no-recreate-import-unsupported", "Block instead of auto-handling resource types AWS does NOT support in IMPORT changesets (currently only AWS::ApiGatewayV2::Stage, emitted by CDK HttpApi). Default behavior: cdkd skips these from phase 1, deletes the AWS-side resource between phases, and lets CFn re-CREATE in phase 2 (brief unavailability window). With this flag, the export aborts with a clear error instead.").action(withErrorHandling(exportCommand));
|
|
61012
|
+
const cmd = new Command("export").description("Hand a cdkd-managed stack over to CloudFormation via CFn IMPORT (changeset). AWS resources are unchanged; cdkd state for the stack is deleted on success. Mirror of `cdkd import` (AWS → cdkd) in the reverse direction (cdkd → CFn). Accepts JSON and YAML templates (YAML via a CFn-aware codec that preserves !Ref / !GetAtt / !Sub shorthand). Aborts if any resource is not CFn-importable.").argument("[stack]", "Stack name to export (auto-detected for single-stack apps)").option("--cfn-stack-name <name>", "Name of the destination CloudFormation stack for the root stack. Defaults to the cdkd stack name (or the cdkd2cfnStackName mapping when the name contains '~'). For per-nested-child overrides, use --cfn-child-stack-name.").option("--cfn-child-stack-name <pair...>", "Per-nested-child CFn stack-name override. Repeatable. Format: '<cdkdName>=<cfnName>' (e.g. --cfn-child-stack-name 'MyApp~Database=my-app-db'). The cdkd stack name for a nested child is '<parent>~<childLogicalId>' (v6 state-key form). Without an override the CFn name is derived by replacing '~' with '-'. Only consulted when the exported stack tree contains nested children; ignored for flat stacks.").option("--template <path>", "Path to a pre-rendered CloudFormation template (JSON or YAML — format auto-detected). Skips synth.").option("--stack-region <region>", "Region of the cdkd state record to operate on. Required when the same stack name has state in multiple regions.").option("--dry-run", "Print the import plan without creating a changeset.", false).option("--accept-transient-context", "Allow CLI -c key=value overrides at export time even though they are not persisted to cdk.json / cdk.context.json (default: refuse). When set, the user is responsible for passing the same -c flags to every future cdk deploy.", false).option("--include-non-importable", "Run a 2-phase migration when the stack contains non-importable resources (Custom::*). Phase 1 imports the importable resources; phase 2 CFn-CREATEs the non-importable ones, which re-invokes each Custom Resource's onCreate handler. Make sure onCreate is idempotent before enabling.", false).option("--parameter <key=value...>", "CFn template Parameter override, repeatable. Required when the synthesized template has Parameters without Default values; otherwise overrides the template's default value. Format: --parameter Key=Value.").option("--strict-cross-stack", "Refuse to export when sibling cdkd stacks in the same CDK app reference the exporting stack via Fn::GetStackOutput. Without the flag, cdkd warns and proceeds — the consumers keep resolving via the CloudFormation fallback (weak reference) unless they deploy with --no-cfn-fallback.", false).option("--skip-import-support-preflight", "Skip the pre-flight that refuses resource types whose CloudFormation registry schema says CFn cannot IMPORT them (no read handler AND NON_PROVISIONABLE), and let CreateChangeSet answer instead. The pre-flight is a registry HEURISTIC, not AWS's supported-for-import list, so this is the escape hatch for a type AWS has since made importable. Expect the changeset to be rejected if the heuristic was right.", false).option("--no-recreate-import-unsupported", "Block instead of auto-handling resource types AWS does NOT support in IMPORT changesets (currently only AWS::ApiGatewayV2::Stage, emitted by CDK HttpApi). Default behavior: cdkd skips these from phase 1, deletes the AWS-side resource between phases, and lets CFn re-CREATE in phase 2 (brief unavailability window). With this flag, the export aborts with a clear error instead.").action(withErrorHandling(exportCommand));
|
|
60680
61013
|
[
|
|
60681
61014
|
...commonOptions,
|
|
60682
61015
|
...appOptions,
|
|
@@ -61356,6 +61689,7 @@ async function stateDestroyCommand(stackArgs, options) {
|
|
|
61356
61689
|
}
|
|
61357
61690
|
logger.info(`Found ${stackNames.length} stack(s) to destroy: ${stackNames.join(", ")}`);
|
|
61358
61691
|
let totalErrors = 0;
|
|
61692
|
+
let totalSkipped = 0;
|
|
61359
61693
|
let interrupted = false;
|
|
61360
61694
|
for (const stackName of stackNames) {
|
|
61361
61695
|
const refs = stateRefs.filter((r) => r.stackName === stackName);
|
|
@@ -61416,6 +61750,7 @@ async function stateDestroyCommand(stackArgs, options) {
|
|
|
61416
61750
|
...options.resourceTimeout?.perTypeMs && { resourceTimeoutByType: options.resourceTimeout.perTypeMs }
|
|
61417
61751
|
}));
|
|
61418
61752
|
totalErrors += result.errorCount;
|
|
61753
|
+
totalSkipped += result.skippedCount;
|
|
61419
61754
|
if (result.interrupted) interrupted = true;
|
|
61420
61755
|
if (interrupted) break;
|
|
61421
61756
|
}
|
|
@@ -61423,6 +61758,7 @@ async function stateDestroyCommand(stackArgs, options) {
|
|
|
61423
61758
|
}
|
|
61424
61759
|
if (totalErrors > 0) throw new PartialFailureError(`Destroy completed with ${totalErrors} resource error(s). State preserved — inspect 'cdkd state show <stack>' and re-run 'cdkd state destroy' to retry. If the same resource keeps failing, 'cdkd state orphan <stack>' removes the state record without deleting AWS resources.`);
|
|
61425
61760
|
if (interrupted) throw new PartialFailureError(`Destroy interrupted by Ctrl-C. State preserved — re-run 'cdkd state destroy' to finish.`);
|
|
61761
|
+
if (totalSkipped > 0) throw new PartialFailureError(`Destroy skipped ${totalSkipped} entr${totalSkipped === 1 ? "y" : "ies"} cdkd could not address, so the underlying resources may still exist in AWS. A skipped nested stack counts as ONE entry and may cover several of its own resources — the per-stack summaries above give the exact breakdown. State preserved (the records are kept). Repair the physicalId in state.json and re-run 'cdkd state destroy', or delete the resources by hand and drop the records with 'cdkd state orphan <stack>'.`);
|
|
61426
61762
|
} finally {
|
|
61427
61763
|
unforwardSigterm();
|
|
61428
61764
|
setup.dispose();
|
|
@@ -62492,6 +62828,11 @@ function printRunList(stackName, region, runs) {
|
|
|
62492
62828
|
logger.info(gray(`\nUse 'cdkd events ${stackName} --run <runId>' to read one run's events.`));
|
|
62493
62829
|
}
|
|
62494
62830
|
/** Human-readable single-run event stream (in recorded order). */
|
|
62831
|
+
/**
|
|
62832
|
+
* Render one run's event stream. Exported for unit-test coverage — the
|
|
62833
|
+
* `counts.skipped` / `reason` rendering is the whole value of those fields
|
|
62834
|
+
* (issue #1752), so it needs a direct fence rather than only `--json` coverage.
|
|
62835
|
+
*/
|
|
62495
62836
|
function printRunEvents(stackName, region, runId, events) {
|
|
62496
62837
|
const logger = getLogger();
|
|
62497
62838
|
logger.info(`${bold("Events for run")} ${cyan(runId)} ${gray(`(${stackName}, ${region})`)}`);
|
|
@@ -62509,8 +62850,9 @@ function printRunEvents(stackName, region, runId, events) {
|
|
|
62509
62850
|
if (e.cdkdVersion) parts.push(gray(`cdkd ${e.cdkdVersion}`));
|
|
62510
62851
|
if (e.result) parts.push(e.result === "SUCCEEDED" ? green(e.result) : red(e.result));
|
|
62511
62852
|
if (typeof e.durationMs === "number") parts.push(gray(`${e.durationMs}ms`));
|
|
62512
|
-
if (e.counts) parts.push(gray(`+${e.counts.created}/~${e.counts.updated}/-${e.counts.deleted}` + (e.counts.failed ? ` !${e.counts.failed}` : "")));
|
|
62853
|
+
if (e.counts) parts.push(gray(`+${e.counts.created}/~${e.counts.updated}/-${e.counts.deleted}` + (e.counts.failed ? ` !${e.counts.failed}` : "") + (e.counts.skipped ? ` ⚠${e.counts.skipped}` : "")));
|
|
62513
62854
|
logger.info(` ${parts.join(" ")}`);
|
|
62855
|
+
if (e.reason) logger.info(` ${yellow(e.reason)}`);
|
|
62514
62856
|
if (e.error) {
|
|
62515
62857
|
const code = e.error.awsErrorCode ? ` (${e.error.awsErrorCode})` : "";
|
|
62516
62858
|
const reqId = e.error.requestId ? gray(` requestId=${e.error.requestId}`) : "";
|
|
@@ -62518,10 +62860,17 @@ function printRunEvents(stackName, region, runId, events) {
|
|
|
62518
62860
|
}
|
|
62519
62861
|
}
|
|
62520
62862
|
}
|
|
62521
|
-
/**
|
|
62863
|
+
/**
|
|
62864
|
+
* Color the event-type token by its lifecycle phase.
|
|
62865
|
+
*
|
|
62866
|
+
* Exported for unit-test coverage (matching `destroy-runner.ts`'s
|
|
62867
|
+
* `PROTECTION_PROPERTY_BY_TYPE` convention) — it is pure, so testing it
|
|
62868
|
+
* directly beats driving it through the renderer's log output.
|
|
62869
|
+
*/
|
|
62522
62870
|
function colorizeEventType(eventType) {
|
|
62523
62871
|
if (eventType.endsWith("FAILED")) return red(eventType);
|
|
62524
62872
|
if (eventType.endsWith("SUCCEEDED") || eventType === "RUN_FINISHED") return green(eventType);
|
|
62873
|
+
if (eventType === "RESOURCE_SKIPPED") return yellow(eventType);
|
|
62525
62874
|
if (eventType.startsWith("ROLLBACK")) return yellow(eventType);
|
|
62526
62875
|
return cyan(eventType);
|
|
62527
62876
|
}
|
|
@@ -74505,7 +74854,7 @@ function createMigrateCommand() {
|
|
|
74505
74854
|
*/
|
|
74506
74855
|
function buildProgram() {
|
|
74507
74856
|
const program = new Command();
|
|
74508
|
-
program.name("cdkd").description("CDK Direct - Deploy AWS CDK apps directly via SDK/Cloud Control API").version("0.281.
|
|
74857
|
+
program.name("cdkd").description("CDK Direct - Deploy AWS CDK apps directly via SDK/Cloud Control API").version("0.281.24");
|
|
74509
74858
|
program.hook("preAction", (_thisCommand, actionCommand) => {
|
|
74510
74859
|
const { profile } = actionCommand.optsWithGlobals();
|
|
74511
74860
|
if (profile !== void 0) process.env["AWS_PROFILE"] = profile;
|