@go-to-k/cdkd 0.143.0 → 0.145.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +5 -2
- package/dist/cli.js +1100 -364
- package/dist/cli.js.map +1 -1
- package/dist/{deploy-engine-Dff3_JMn.js → deploy-engine-DjnWyAAc.js} +32 -8
- package/dist/deploy-engine-DjnWyAAc.js.map +1 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/package.json +1 -1
- package/dist/deploy-engine-Dff3_JMn.js.map +0 -1
package/dist/cli.js
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { _ as withSkipPrefix, a as runDockerStreaming, c as getLogger, d as getLiveRenderer, f as PATTERN_B_NAME_PROPERTIES, g as generateResourceNameWithFallback, h as generateResourceName, i as runDockerForeground, n as formatDockerLoginError, p as PATTERN_B_RESOURCE_TYPES, r as getDockerCmd, u as runStackBuffered, v as withStackName } from "./docker-cmd-EtWSTAje.js";
|
|
3
|
-
import { A as AssetPublisher, B as resolveStateBucketWithDefault, C as applyRoleArnIfSet, D as LockManager, E as TemplateParser, F as getDefaultStateBucketName, G as MIGRATE_TMP_PREFIX, H as warnDeprecatedNoPrefixCliFlag, I as getLegacyStateBucketName, J as AssemblyReader, K as findLargeInlineResources, L as resolveApp, M as WorkGraph, N as buildDockerImage, O as S3StateBackend, P as Synthesizer, Q as CdkdError, R as resolveCaptureObservedState, S as IntrinsicFunctionResolver, T as DagBuilder, U as CFN_TEMPLATE_BODY_LIMIT, V as resolveStateBucketWithDefaultAndSource, W as CFN_TEMPLATE_URL_LIMIT, X as resolveBucketRegion, _ as normalizeAwsTagsToCfn, _t as normalizeAwsError, a as withRetry, at as MissingCdkCliError, b as CloudControlProvider, c as cyan, ct as ResourceTimeoutError, d as red, dt as StackHasActiveImportsError, f as yellow, ft as StackTerminationProtectionError, g as matchesCdkPath, h as CDK_PATH_TAG, i as withResourceDeadline, j as stringifyValue, k as shouldRetainResource, l as gray, lt as ResourceUpdateNotSupportedError, m as collectInlinePolicyNamesManagedBySiblings, n as DEFAULT_RESOURCE_WARN_AFTER_MS, nt as LocalMigrateError, o as IMPLICIT_DELETE_DEPENDENCIES, ot as PartialFailureError, p as IAMRoleProvider, q as uploadCfnTemplate, r as DeployEngine, rt as LocalStartServiceError, s as bold, st as ProvisioningError, t as DEFAULT_RESOURCE_TIMEOUT_MS, tt as LocalInvokeBuildError, u as green, ut as RouteDiscoveryError, v as resolveExplicitPhysicalId, vt as withErrorHandling, w as DiffCalculator, x as assertRegionMatch, y as ProviderRegistry, z as resolveSkipPrefix } from "./deploy-engine-
|
|
3
|
+
import { A as AssetPublisher, B as resolveStateBucketWithDefault, C as applyRoleArnIfSet, D as LockManager, E as TemplateParser, F as getDefaultStateBucketName, G as MIGRATE_TMP_PREFIX, H as warnDeprecatedNoPrefixCliFlag, I as getLegacyStateBucketName, J as AssemblyReader, K as findLargeInlineResources, L as resolveApp, M as WorkGraph, N as buildDockerImage, O as S3StateBackend, P as Synthesizer, Q as CdkdError, R as resolveCaptureObservedState, S as IntrinsicFunctionResolver, T as DagBuilder, U as CFN_TEMPLATE_BODY_LIMIT, V as resolveStateBucketWithDefaultAndSource, W as CFN_TEMPLATE_URL_LIMIT, X as resolveBucketRegion, _ as normalizeAwsTagsToCfn, _t as normalizeAwsError, a as withRetry, at as MissingCdkCliError, b as CloudControlProvider, c as cyan, ct as ResourceTimeoutError, d as red, dt as StackHasActiveImportsError, f as yellow, ft as StackTerminationProtectionError, g as matchesCdkPath, h as CDK_PATH_TAG, i as withResourceDeadline, j as stringifyValue, k as shouldRetainResource, l as gray, lt as ResourceUpdateNotSupportedError, m as collectInlinePolicyNamesManagedBySiblings, n as DEFAULT_RESOURCE_WARN_AFTER_MS, nt as LocalMigrateError, o as IMPLICIT_DELETE_DEPENDENCIES, ot as PartialFailureError, p as IAMRoleProvider, q as uploadCfnTemplate, r as DeployEngine, rt as LocalStartServiceError, s as bold, st as ProvisioningError, t as DEFAULT_RESOURCE_TIMEOUT_MS, tt as LocalInvokeBuildError, u as green, ut as RouteDiscoveryError, v as resolveExplicitPhysicalId, vt as withErrorHandling, w as DiffCalculator, x as assertRegionMatch, y as ProviderRegistry, z as resolveSkipPrefix } from "./deploy-engine-DjnWyAAc.js";
|
|
4
4
|
import { a as setAwsClients, i as resetAwsClients, r as getAwsClients, t as AwsClients } from "./aws-clients-BF03Alpe.js";
|
|
5
|
+
import { AsyncLocalStorage } from "node:async_hooks";
|
|
5
6
|
import { createHash, createHmac, createPublicKey, createVerify, randomBytes, randomUUID, timingSafeEqual } from "node:crypto";
|
|
6
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";
|
|
7
8
|
import { AddRoleToInstanceProfileCommand, AddUserToGroupCommand, AttachGroupPolicyCommand, AttachUserPolicyCommand, CreateGroupCommand, CreateInstanceProfileCommand, CreateLoginProfileCommand, CreateUserCommand, DeleteAccessKeyCommand, DeleteGroupCommand, DeleteGroupPolicyCommand, DeleteInstanceProfileCommand, DeleteLoginProfileCommand, DeleteRolePolicyCommand, DeleteUserCommand, DeleteUserPermissionsBoundaryCommand, DeleteUserPolicyCommand, DetachGroupPolicyCommand, DetachUserPolicyCommand, GetGroupCommand, GetGroupPolicyCommand, GetInstanceProfileCommand, GetRolePolicyCommand, GetUserCommand, GetUserPolicyCommand, IAMClient, ListAccessKeysCommand, ListAttachedGroupPoliciesCommand, ListAttachedUserPoliciesCommand, ListGroupPoliciesCommand, ListGroupsForUserCommand, ListInstanceProfilesCommand, ListUserPoliciesCommand, ListUserTagsCommand, ListUsersCommand, NoSuchEntityException, PutGroupPolicyCommand, PutRolePolicyCommand, PutUserPermissionsBoundaryCommand, PutUserPolicyCommand, RemoveRoleFromInstanceProfileCommand, RemoveUserFromGroupCommand, TagUserCommand, UntagUserCommand, UpdateLoginProfileCommand } from "@aws-sdk/client-iam";
|
|
@@ -20,6 +21,7 @@ import { CloudFrontClient, CreateCloudFrontOriginAccessIdentityCommand, CreateDi
|
|
|
20
21
|
import { CloudWatchClient, DeleteAlarmsCommand, DescribeAlarmsCommand, ListTagsForResourceCommand as ListTagsForResourceCommand$4, PutMetricAlarmCommand, TagResourceCommand as TagResourceCommand$6, UntagResourceCommand as UntagResourceCommand$6 } from "@aws-sdk/client-cloudwatch";
|
|
21
22
|
import { CloudWatchLogsClient, CreateLogGroupCommand, DeleteDataProtectionPolicyCommand, DeleteIndexPolicyCommand, DeleteLogGroupCommand, DeleteRetentionPolicyCommand, DescribeIndexPoliciesCommand, DescribeLogGroupsCommand, GetDataProtectionPolicyCommand, ListTagsForResourceCommand as ListTagsForResourceCommand$5, PutBearerTokenAuthenticationCommand, PutDataProtectionPolicyCommand, PutIndexPolicyCommand, PutLogGroupDeletionProtectionCommand, PutRetentionPolicyCommand, ResourceAlreadyExistsException, ResourceNotFoundException as ResourceNotFoundException$4, TagResourceCommand as TagResourceCommand$7, UntagResourceCommand as UntagResourceCommand$7 } from "@aws-sdk/client-cloudwatch-logs";
|
|
22
23
|
import { BedrockAgentCoreControlClient, CreateAgentRuntimeCommand, DeleteAgentRuntimeCommand, GetAgentRuntimeCommand, ResourceNotFoundException as ResourceNotFoundException$5, UpdateAgentRuntimeCommand } from "@aws-sdk/client-bedrock-agentcore-control";
|
|
24
|
+
import * as fs from "node:fs";
|
|
23
25
|
import { cpSync, createWriteStream, existsSync, mkdirSync, mkdtempSync, readFileSync, readdirSync, rmSync, statSync, writeFileSync } from "node:fs";
|
|
24
26
|
import * as path from "node:path";
|
|
25
27
|
import { dirname, isAbsolute, join, normalize, resolve } from "node:path";
|
|
@@ -27403,7 +27405,17 @@ var EFSProvider = class {
|
|
|
27403
27405
|
* Membership grows as each follow-up PR lands. Keep alphabetical
|
|
27404
27406
|
* to minimize merge conflicts when multiple follow-ups race.
|
|
27405
27407
|
*/
|
|
27406
|
-
const SUPPORTED_UPDATE_DESTINATIONS = new Set([
|
|
27408
|
+
const SUPPORTED_UPDATE_DESTINATIONS = new Set([
|
|
27409
|
+
"AmazonOpenSearchServerlessDestinationConfiguration",
|
|
27410
|
+
"AmazonopensearchserviceDestinationConfiguration",
|
|
27411
|
+
"ElasticsearchDestinationConfiguration",
|
|
27412
|
+
"ExtendedS3DestinationConfiguration",
|
|
27413
|
+
"HttpEndpointDestinationConfiguration",
|
|
27414
|
+
"IcebergDestinationConfiguration",
|
|
27415
|
+
"RedshiftDestinationConfiguration",
|
|
27416
|
+
"SnowflakeDestinationConfiguration",
|
|
27417
|
+
"SplunkDestinationConfiguration"
|
|
27418
|
+
]);
|
|
27407
27419
|
/**
|
|
27408
27420
|
* SDK Provider for AWS Kinesis Firehose resources
|
|
27409
27421
|
*
|
|
@@ -27607,18 +27619,85 @@ var FirehoseProvider = class {
|
|
|
27607
27619
|
* `CloudWatchLoggingOptions`, `Username` / `Password` /
|
|
27608
27620
|
* `RoleARN` / `ClusterJDBCURL`.
|
|
27609
27621
|
*
|
|
27610
|
-
*
|
|
27611
|
-
* `
|
|
27612
|
-
* `
|
|
27613
|
-
* `
|
|
27614
|
-
* `
|
|
27615
|
-
* `
|
|
27616
|
-
* `
|
|
27617
|
-
* `
|
|
27618
|
-
*
|
|
27619
|
-
*
|
|
27620
|
-
*
|
|
27621
|
-
*
|
|
27622
|
+
* - `SplunkDestinationConfiguration` (#549) — same flow as
|
|
27623
|
+
* Redshift: `DescribeDeliveryStream` recovers VersionId +
|
|
27624
|
+
* DestinationId, then `UpdateDestinationCommand` issues the
|
|
27625
|
+
* diff with a `SplunkDestinationUpdate` payload produced by
|
|
27626
|
+
* {@link mapSplunkConfigToUpdate}. Handles `HECEndpoint`,
|
|
27627
|
+
* `HECEndpointType`, `HECToken`,
|
|
27628
|
+
* `HECAcknowledgmentTimeoutInSeconds`, `RetryOptions`,
|
|
27629
|
+
* `S3BackupMode`, `S3Configuration` → `S3Update`,
|
|
27630
|
+
* `ProcessingConfiguration`, `CloudWatchLoggingOptions`,
|
|
27631
|
+
* `BufferingHints`, `SecretsManagerConfiguration`. Splunk has
|
|
27632
|
+
* no S3 backup destination shape (no `S3BackupConfiguration`
|
|
27633
|
+
* field) — unlike Redshift / ExtendedS3.
|
|
27634
|
+
*
|
|
27635
|
+
* - `AmazonopensearchserviceDestinationConfiguration` (#549) —
|
|
27636
|
+
* `AmazonopensearchserviceDestinationUpdate` payload produced
|
|
27637
|
+
* by {@link mapAmazonopensearchserviceConfigToUpdate}. Handles
|
|
27638
|
+
* `RoleARN`, `DomainARN`, `ClusterEndpoint`, `IndexName`,
|
|
27639
|
+
* `TypeName`, `IndexRotationPeriod`, `BufferingHints`,
|
|
27640
|
+
* `RetryOptions`, `S3Configuration` → `S3Update`,
|
|
27641
|
+
* `ProcessingConfiguration`, `CloudWatchLoggingOptions`,
|
|
27642
|
+
* `DocumentIdOptions`. `VpcConfiguration` is read-only on
|
|
27643
|
+
* AWS-side Update — diffs to that field will not be forwarded
|
|
27644
|
+
* and surface on the next `cdkd drift` run.
|
|
27645
|
+
*
|
|
27646
|
+
* - `AmazonOpenSearchServerlessDestinationConfiguration` (#549) —
|
|
27647
|
+
* `AmazonOpenSearchServerlessDestinationUpdate` payload
|
|
27648
|
+
* produced by {@link mapAmazonOpenSearchServerlessConfigToUpdate}.
|
|
27649
|
+
* Simpler than the service variant — `RoleARN`,
|
|
27650
|
+
* `CollectionEndpoint`, `IndexName`, `BufferingHints`,
|
|
27651
|
+
* `RetryOptions`, `S3Configuration` → `S3Update`,
|
|
27652
|
+
* `ProcessingConfiguration`, `CloudWatchLoggingOptions`.
|
|
27653
|
+
*
|
|
27654
|
+
* - `HttpEndpointDestinationConfiguration` (#549) —
|
|
27655
|
+
* `HttpEndpointDestinationUpdate` payload produced by
|
|
27656
|
+
* {@link mapHttpEndpointConfigToUpdate}. Handles
|
|
27657
|
+
* `EndpointConfiguration`, `BufferingHints`,
|
|
27658
|
+
* `CloudWatchLoggingOptions`, `RequestConfiguration`,
|
|
27659
|
+
* `ProcessingConfiguration`, `RoleARN`, `RetryOptions`,
|
|
27660
|
+
* `S3BackupMode`, `S3Configuration` → `S3Update`,
|
|
27661
|
+
* `SecretsManagerConfiguration`.
|
|
27662
|
+
*
|
|
27663
|
+
* - `ElasticsearchDestinationConfiguration` (#549) —
|
|
27664
|
+
* `ElasticsearchDestinationUpdate` payload produced by
|
|
27665
|
+
* {@link mapElasticsearchConfigToUpdate}. Legacy variant of
|
|
27666
|
+
* `Amazonopensearchservice*`; same field shape sans
|
|
27667
|
+
* `VpcConfiguration` (also read-only on Update).
|
|
27668
|
+
*
|
|
27669
|
+
* - `IcebergDestinationConfiguration` (#549) —
|
|
27670
|
+
* `IcebergDestinationUpdate` payload produced by
|
|
27671
|
+
* {@link mapIcebergConfigToUpdate}. Handles
|
|
27672
|
+
* `DestinationTableConfigurationList`,
|
|
27673
|
+
* `SchemaEvolutionConfiguration`,
|
|
27674
|
+
* `TableCreationConfiguration`, `BufferingHints`,
|
|
27675
|
+
* `CloudWatchLoggingOptions`, `ProcessingConfiguration`,
|
|
27676
|
+
* `S3BackupMode`, `RetryOptions`, `RoleARN`, `AppendOnly`,
|
|
27677
|
+
* `CatalogConfiguration`, `S3Configuration`. **Quirk**: the
|
|
27678
|
+
* SDK Update shape's S3 field is named `S3Configuration` (full
|
|
27679
|
+
* `S3DestinationConfiguration` shape), NOT `S3Update` — unlike
|
|
27680
|
+
* every other destination type. The reverse-mapper forwards it
|
|
27681
|
+
* verbatim without renaming.
|
|
27682
|
+
*
|
|
27683
|
+
* - `SnowflakeDestinationConfiguration` (#549) —
|
|
27684
|
+
* `SnowflakeDestinationUpdate` payload produced by
|
|
27685
|
+
* {@link mapSnowflakeConfigToUpdate}. Handles many connector
|
|
27686
|
+
* credentials (`AccountUrl` / `PrivateKey` / `KeyPassphrase` /
|
|
27687
|
+
* `User` / `Database` / `Schema` / `Table` /
|
|
27688
|
+
* `SnowflakeRoleConfiguration`) plus the standard suite
|
|
27689
|
+
* (`DataLoadingOption`, `MetaDataColumnName`,
|
|
27690
|
+
* `ContentColumnName`, `CloudWatchLoggingOptions`,
|
|
27691
|
+
* `ProcessingConfiguration`, `RoleARN`, `RetryOptions`,
|
|
27692
|
+
* `S3BackupMode`, `S3Configuration` → `S3Update`,
|
|
27693
|
+
* `SecretsManagerConfiguration`, `BufferingHints`).
|
|
27694
|
+
*
|
|
27695
|
+
* The legacy `S3DestinationConfiguration` (deprecated by AWS in
|
|
27696
|
+
* favor of `ExtendedS3DestinationConfiguration`) stays rejected
|
|
27697
|
+
* with a tightened error — CDK constructs always emit Extended.
|
|
27698
|
+
* Templates that still pin the legacy shape should migrate to
|
|
27699
|
+
* Extended; in-place update of the deprecated shape isn't a
|
|
27700
|
+
* priority follow-up.
|
|
27622
27701
|
*
|
|
27623
27702
|
* Destination-type SWITCHES (e.g. ExtendedS3 → Redshift) are immutable
|
|
27624
27703
|
* on AWS; cdkd surfaces `ResourceUpdateNotSupportedError` so the caller
|
|
@@ -27646,6 +27725,41 @@ var FirehoseProvider = class {
|
|
|
27646
27725
|
const prevDest = previousProperties[activeDest] ?? {};
|
|
27647
27726
|
if (JSON.stringify(nextDest) !== JSON.stringify(prevDest)) await this.applyRedshiftDestinationUpdate(physicalId, nextDest);
|
|
27648
27727
|
}
|
|
27728
|
+
if (activeDest === "SplunkDestinationConfiguration") {
|
|
27729
|
+
const nextDest = properties[activeDest] ?? {};
|
|
27730
|
+
const prevDest = previousProperties[activeDest] ?? {};
|
|
27731
|
+
if (JSON.stringify(nextDest) !== JSON.stringify(prevDest)) await this.applySplunkDestinationUpdate(physicalId, nextDest);
|
|
27732
|
+
}
|
|
27733
|
+
if (activeDest === "AmazonopensearchserviceDestinationConfiguration") {
|
|
27734
|
+
const nextDest = properties[activeDest] ?? {};
|
|
27735
|
+
const prevDest = previousProperties[activeDest] ?? {};
|
|
27736
|
+
if (JSON.stringify(nextDest) !== JSON.stringify(prevDest)) await this.applyAmazonopensearchserviceDestinationUpdate(physicalId, nextDest);
|
|
27737
|
+
}
|
|
27738
|
+
if (activeDest === "AmazonOpenSearchServerlessDestinationConfiguration") {
|
|
27739
|
+
const nextDest = properties[activeDest] ?? {};
|
|
27740
|
+
const prevDest = previousProperties[activeDest] ?? {};
|
|
27741
|
+
if (JSON.stringify(nextDest) !== JSON.stringify(prevDest)) await this.applyAmazonOpenSearchServerlessDestinationUpdate(physicalId, nextDest);
|
|
27742
|
+
}
|
|
27743
|
+
if (activeDest === "HttpEndpointDestinationConfiguration") {
|
|
27744
|
+
const nextDest = properties[activeDest] ?? {};
|
|
27745
|
+
const prevDest = previousProperties[activeDest] ?? {};
|
|
27746
|
+
if (JSON.stringify(nextDest) !== JSON.stringify(prevDest)) await this.applyHttpEndpointDestinationUpdate(physicalId, nextDest);
|
|
27747
|
+
}
|
|
27748
|
+
if (activeDest === "ElasticsearchDestinationConfiguration") {
|
|
27749
|
+
const nextDest = properties[activeDest] ?? {};
|
|
27750
|
+
const prevDest = previousProperties[activeDest] ?? {};
|
|
27751
|
+
if (JSON.stringify(nextDest) !== JSON.stringify(prevDest)) await this.applyElasticsearchDestinationUpdate(physicalId, nextDest);
|
|
27752
|
+
}
|
|
27753
|
+
if (activeDest === "IcebergDestinationConfiguration") {
|
|
27754
|
+
const nextDest = properties[activeDest] ?? {};
|
|
27755
|
+
const prevDest = previousProperties[activeDest] ?? {};
|
|
27756
|
+
if (JSON.stringify(nextDest) !== JSON.stringify(prevDest)) await this.applyIcebergDestinationUpdate(physicalId, nextDest);
|
|
27757
|
+
}
|
|
27758
|
+
if (activeDest === "SnowflakeDestinationConfiguration") {
|
|
27759
|
+
const nextDest = properties[activeDest] ?? {};
|
|
27760
|
+
const prevDest = previousProperties[activeDest] ?? {};
|
|
27761
|
+
if (JSON.stringify(nextDest) !== JSON.stringify(prevDest)) await this.applySnowflakeDestinationUpdate(physicalId, nextDest);
|
|
27762
|
+
}
|
|
27649
27763
|
const desc = (await this.getClient().send(new DescribeDeliveryStreamCommand({ DeliveryStreamName: physicalId }))).DeliveryStreamDescription;
|
|
27650
27764
|
return {
|
|
27651
27765
|
physicalId,
|
|
@@ -27887,6 +28001,304 @@ var FirehoseProvider = class {
|
|
|
27887
28001
|
return result;
|
|
27888
28002
|
}
|
|
27889
28003
|
/**
|
|
28004
|
+
* Recover `CurrentDeliveryStreamVersionId` + `DestinationId` from
|
|
28005
|
+
* `DescribeDeliveryStream` and issue `UpdateDestination` with the new
|
|
28006
|
+
* Splunk shape. The reverse-mapper at
|
|
28007
|
+
* {@link mapSplunkConfigToUpdate} produces the
|
|
28008
|
+
* `SplunkDestinationUpdate` payload — only fields present in the
|
|
28009
|
+
* input are forwarded so omitted CFn keys don't clobber AWS-side
|
|
28010
|
+
* state. Mirrors {@link applyRedshiftDestinationUpdate} (#549).
|
|
28011
|
+
*/
|
|
28012
|
+
async applySplunkDestinationUpdate(physicalId, nextConfig) {
|
|
28013
|
+
const desc = (await this.getClient().send(new DescribeDeliveryStreamCommand({ DeliveryStreamName: physicalId }))).DeliveryStreamDescription;
|
|
28014
|
+
const currentVersionId = desc?.VersionId;
|
|
28015
|
+
const destinationId = (desc?.Destinations?.[0])?.DestinationId;
|
|
28016
|
+
if (!currentVersionId || !destinationId) throw new ProvisioningError(`DescribeDeliveryStream for ${physicalId} did not return VersionId or DestinationId; UpdateDestination cannot proceed.`, "AWS::KinesisFirehose::DeliveryStream", physicalId);
|
|
28017
|
+
await this.getClient().send(new UpdateDestinationCommand({
|
|
28018
|
+
DeliveryStreamName: physicalId,
|
|
28019
|
+
CurrentDeliveryStreamVersionId: currentVersionId,
|
|
28020
|
+
DestinationId: destinationId,
|
|
28021
|
+
SplunkDestinationUpdate: this.mapSplunkConfigToUpdate(nextConfig)
|
|
28022
|
+
}));
|
|
28023
|
+
}
|
|
28024
|
+
/**
|
|
28025
|
+
* Map CFn `SplunkDestinationConfiguration` to the
|
|
28026
|
+
* `SplunkDestinationUpdate` shape used by AWS
|
|
28027
|
+
* `UpdateDestinationCommand` (#549). Every field is `!== undefined`
|
|
28028
|
+
* gated so omitted CFn keys do not clobber AWS-side state. The CFn
|
|
28029
|
+
* `S3Configuration` field is mapped through
|
|
28030
|
+
* {@link mapS3ConfigToUpdate} into the SDK-side `S3Update` slot.
|
|
28031
|
+
* Splunk has no `S3BackupConfiguration` field (unlike Redshift /
|
|
28032
|
+
* ExtendedS3).
|
|
28033
|
+
*/
|
|
28034
|
+
mapSplunkConfigToUpdate(config) {
|
|
28035
|
+
const result = {};
|
|
28036
|
+
if (config["HECEndpoint"] !== void 0) result.HECEndpoint = config["HECEndpoint"];
|
|
28037
|
+
if (config["HECEndpointType"] !== void 0) result.HECEndpointType = config["HECEndpointType"];
|
|
28038
|
+
if (config["HECToken"] !== void 0) result.HECToken = config["HECToken"];
|
|
28039
|
+
if (config["HECAcknowledgmentTimeoutInSeconds"] !== void 0) result.HECAcknowledgmentTimeoutInSeconds = config["HECAcknowledgmentTimeoutInSeconds"];
|
|
28040
|
+
if (config["RetryOptions"] !== void 0) result.RetryOptions = config["RetryOptions"];
|
|
28041
|
+
if (config["S3BackupMode"] !== void 0) result.S3BackupMode = config["S3BackupMode"];
|
|
28042
|
+
if (config["S3Configuration"] !== void 0) result.S3Update = this.mapS3ConfigToUpdate(config["S3Configuration"]);
|
|
28043
|
+
if (config["ProcessingConfiguration"] !== void 0) result.ProcessingConfiguration = config["ProcessingConfiguration"];
|
|
28044
|
+
if (config["CloudWatchLoggingOptions"] !== void 0) result.CloudWatchLoggingOptions = config["CloudWatchLoggingOptions"];
|
|
28045
|
+
if (config["BufferingHints"] !== void 0) result.BufferingHints = config["BufferingHints"];
|
|
28046
|
+
if (config["SecretsManagerConfiguration"] !== void 0) result.SecretsManagerConfiguration = config["SecretsManagerConfiguration"];
|
|
28047
|
+
return result;
|
|
28048
|
+
}
|
|
28049
|
+
/**
|
|
28050
|
+
* Apply UpdateDestination for `AmazonopensearchserviceDestinationConfiguration` (#549).
|
|
28051
|
+
* Mirrors {@link applyRedshiftDestinationUpdate}.
|
|
28052
|
+
*/
|
|
28053
|
+
async applyAmazonopensearchserviceDestinationUpdate(physicalId, nextConfig) {
|
|
28054
|
+
const desc = (await this.getClient().send(new DescribeDeliveryStreamCommand({ DeliveryStreamName: physicalId }))).DeliveryStreamDescription;
|
|
28055
|
+
const currentVersionId = desc?.VersionId;
|
|
28056
|
+
const destinationId = desc?.Destinations?.[0]?.DestinationId;
|
|
28057
|
+
if (!currentVersionId || !destinationId) throw new ProvisioningError(`DescribeDeliveryStream for ${physicalId} did not return VersionId or DestinationId; UpdateDestination cannot proceed.`, "AWS::KinesisFirehose::DeliveryStream", physicalId);
|
|
28058
|
+
await this.getClient().send(new UpdateDestinationCommand({
|
|
28059
|
+
DeliveryStreamName: physicalId,
|
|
28060
|
+
CurrentDeliveryStreamVersionId: currentVersionId,
|
|
28061
|
+
DestinationId: destinationId,
|
|
28062
|
+
AmazonopensearchserviceDestinationUpdate: this.mapAmazonopensearchserviceConfigToUpdate(nextConfig)
|
|
28063
|
+
}));
|
|
28064
|
+
}
|
|
28065
|
+
/**
|
|
28066
|
+
* Map CFn `AmazonopensearchserviceDestinationConfiguration` to the
|
|
28067
|
+
* `AmazonopensearchserviceDestinationUpdate` shape (#549). Every field
|
|
28068
|
+
* `!== undefined` gated. CFn `S3Configuration` → SDK `S3Update`.
|
|
28069
|
+
* `VpcConfiguration` is intentionally NOT included — the SDK Update
|
|
28070
|
+
* shape has no field for it (AWS treats Vpc placement as immutable
|
|
28071
|
+
* post-create). VpcConfiguration-only diffs will surface on the next
|
|
28072
|
+
* `cdkd drift` run if the user re-applies a different Vpc on the
|
|
28073
|
+
* console side.
|
|
28074
|
+
*/
|
|
28075
|
+
mapAmazonopensearchserviceConfigToUpdate(config) {
|
|
28076
|
+
const result = {};
|
|
28077
|
+
const roleArn = config["RoleARN"] ?? config["RoleArn"];
|
|
28078
|
+
if (roleArn !== void 0) result.RoleARN = roleArn;
|
|
28079
|
+
const domainArn = config["DomainARN"] ?? config["DomainArn"];
|
|
28080
|
+
if (domainArn !== void 0) result.DomainARN = domainArn;
|
|
28081
|
+
if (config["ClusterEndpoint"] !== void 0) result.ClusterEndpoint = config["ClusterEndpoint"];
|
|
28082
|
+
if (config["IndexName"] !== void 0) result.IndexName = config["IndexName"];
|
|
28083
|
+
if (config["TypeName"] !== void 0) result.TypeName = config["TypeName"];
|
|
28084
|
+
if (config["IndexRotationPeriod"] !== void 0) result.IndexRotationPeriod = config["IndexRotationPeriod"];
|
|
28085
|
+
if (config["BufferingHints"] !== void 0) result.BufferingHints = config["BufferingHints"];
|
|
28086
|
+
if (config["RetryOptions"] !== void 0) result.RetryOptions = config["RetryOptions"];
|
|
28087
|
+
if (config["S3Configuration"] !== void 0) result.S3Update = this.mapS3ConfigToUpdate(config["S3Configuration"]);
|
|
28088
|
+
if (config["ProcessingConfiguration"] !== void 0) result.ProcessingConfiguration = config["ProcessingConfiguration"];
|
|
28089
|
+
if (config["CloudWatchLoggingOptions"] !== void 0) result.CloudWatchLoggingOptions = config["CloudWatchLoggingOptions"];
|
|
28090
|
+
if (config["DocumentIdOptions"] !== void 0) result.DocumentIdOptions = config["DocumentIdOptions"];
|
|
28091
|
+
return result;
|
|
28092
|
+
}
|
|
28093
|
+
/**
|
|
28094
|
+
* Apply UpdateDestination for `AmazonOpenSearchServerlessDestinationConfiguration` (#549).
|
|
28095
|
+
* Mirrors {@link applyRedshiftDestinationUpdate}.
|
|
28096
|
+
*/
|
|
28097
|
+
async applyAmazonOpenSearchServerlessDestinationUpdate(physicalId, nextConfig) {
|
|
28098
|
+
const desc = (await this.getClient().send(new DescribeDeliveryStreamCommand({ DeliveryStreamName: physicalId }))).DeliveryStreamDescription;
|
|
28099
|
+
const currentVersionId = desc?.VersionId;
|
|
28100
|
+
const destinationId = desc?.Destinations?.[0]?.DestinationId;
|
|
28101
|
+
if (!currentVersionId || !destinationId) throw new ProvisioningError(`DescribeDeliveryStream for ${physicalId} did not return VersionId or DestinationId; UpdateDestination cannot proceed.`, "AWS::KinesisFirehose::DeliveryStream", physicalId);
|
|
28102
|
+
await this.getClient().send(new UpdateDestinationCommand({
|
|
28103
|
+
DeliveryStreamName: physicalId,
|
|
28104
|
+
CurrentDeliveryStreamVersionId: currentVersionId,
|
|
28105
|
+
DestinationId: destinationId,
|
|
28106
|
+
AmazonOpenSearchServerlessDestinationUpdate: this.mapAmazonOpenSearchServerlessConfigToUpdate(nextConfig)
|
|
28107
|
+
}));
|
|
28108
|
+
}
|
|
28109
|
+
/**
|
|
28110
|
+
* Map CFn `AmazonOpenSearchServerlessDestinationConfiguration` to the
|
|
28111
|
+
* `AmazonOpenSearchServerlessDestinationUpdate` shape (#549). Every
|
|
28112
|
+
* field `!== undefined` gated. CFn `S3Configuration` → SDK `S3Update`.
|
|
28113
|
+
* Simpler than the service variant — no VpcConfiguration / TypeName /
|
|
28114
|
+
* IndexRotationPeriod / DocumentIdOptions.
|
|
28115
|
+
*/
|
|
28116
|
+
mapAmazonOpenSearchServerlessConfigToUpdate(config) {
|
|
28117
|
+
const result = {};
|
|
28118
|
+
const roleArn = config["RoleARN"] ?? config["RoleArn"];
|
|
28119
|
+
if (roleArn !== void 0) result.RoleARN = roleArn;
|
|
28120
|
+
if (config["CollectionEndpoint"] !== void 0) result.CollectionEndpoint = config["CollectionEndpoint"];
|
|
28121
|
+
if (config["IndexName"] !== void 0) result.IndexName = config["IndexName"];
|
|
28122
|
+
if (config["BufferingHints"] !== void 0) result.BufferingHints = config["BufferingHints"];
|
|
28123
|
+
if (config["RetryOptions"] !== void 0) result.RetryOptions = config["RetryOptions"];
|
|
28124
|
+
if (config["S3Configuration"] !== void 0) result.S3Update = this.mapS3ConfigToUpdate(config["S3Configuration"]);
|
|
28125
|
+
if (config["ProcessingConfiguration"] !== void 0) result.ProcessingConfiguration = config["ProcessingConfiguration"];
|
|
28126
|
+
if (config["CloudWatchLoggingOptions"] !== void 0) result.CloudWatchLoggingOptions = config["CloudWatchLoggingOptions"];
|
|
28127
|
+
return result;
|
|
28128
|
+
}
|
|
28129
|
+
/**
|
|
28130
|
+
* Apply UpdateDestination for `HttpEndpointDestinationConfiguration` (#549).
|
|
28131
|
+
* Mirrors {@link applyRedshiftDestinationUpdate}.
|
|
28132
|
+
*/
|
|
28133
|
+
async applyHttpEndpointDestinationUpdate(physicalId, nextConfig) {
|
|
28134
|
+
const desc = (await this.getClient().send(new DescribeDeliveryStreamCommand({ DeliveryStreamName: physicalId }))).DeliveryStreamDescription;
|
|
28135
|
+
const currentVersionId = desc?.VersionId;
|
|
28136
|
+
const destinationId = desc?.Destinations?.[0]?.DestinationId;
|
|
28137
|
+
if (!currentVersionId || !destinationId) throw new ProvisioningError(`DescribeDeliveryStream for ${physicalId} did not return VersionId or DestinationId; UpdateDestination cannot proceed.`, "AWS::KinesisFirehose::DeliveryStream", physicalId);
|
|
28138
|
+
await this.getClient().send(new UpdateDestinationCommand({
|
|
28139
|
+
DeliveryStreamName: physicalId,
|
|
28140
|
+
CurrentDeliveryStreamVersionId: currentVersionId,
|
|
28141
|
+
DestinationId: destinationId,
|
|
28142
|
+
HttpEndpointDestinationUpdate: this.mapHttpEndpointConfigToUpdate(nextConfig)
|
|
28143
|
+
}));
|
|
28144
|
+
}
|
|
28145
|
+
/**
|
|
28146
|
+
* Map CFn `HttpEndpointDestinationConfiguration` to the
|
|
28147
|
+
* `HttpEndpointDestinationUpdate` shape (#549). Every field
|
|
28148
|
+
* `!== undefined` gated. CFn `S3Configuration` → SDK `S3Update`.
|
|
28149
|
+
* `EndpointConfiguration` (Url / Name / AccessKey) and
|
|
28150
|
+
* `SecretsManagerConfiguration` are both pass-through (AWS accepts
|
|
28151
|
+
* either auth mode on Update).
|
|
28152
|
+
*/
|
|
28153
|
+
mapHttpEndpointConfigToUpdate(config) {
|
|
28154
|
+
const result = {};
|
|
28155
|
+
if (config["EndpointConfiguration"] !== void 0) result.EndpointConfiguration = config["EndpointConfiguration"];
|
|
28156
|
+
if (config["BufferingHints"] !== void 0) result.BufferingHints = config["BufferingHints"];
|
|
28157
|
+
if (config["CloudWatchLoggingOptions"] !== void 0) result.CloudWatchLoggingOptions = config["CloudWatchLoggingOptions"];
|
|
28158
|
+
if (config["RequestConfiguration"] !== void 0) result.RequestConfiguration = config["RequestConfiguration"];
|
|
28159
|
+
if (config["ProcessingConfiguration"] !== void 0) result.ProcessingConfiguration = config["ProcessingConfiguration"];
|
|
28160
|
+
const roleArn = config["RoleARN"] ?? config["RoleArn"];
|
|
28161
|
+
if (roleArn !== void 0) result.RoleARN = roleArn;
|
|
28162
|
+
if (config["RetryOptions"] !== void 0) result.RetryOptions = config["RetryOptions"];
|
|
28163
|
+
if (config["S3BackupMode"] !== void 0) result.S3BackupMode = config["S3BackupMode"];
|
|
28164
|
+
if (config["S3Configuration"] !== void 0) result.S3Update = this.mapS3ConfigToUpdate(config["S3Configuration"]);
|
|
28165
|
+
if (config["SecretsManagerConfiguration"] !== void 0) result.SecretsManagerConfiguration = config["SecretsManagerConfiguration"];
|
|
28166
|
+
return result;
|
|
28167
|
+
}
|
|
28168
|
+
/**
|
|
28169
|
+
* Apply UpdateDestination for `ElasticsearchDestinationConfiguration` (#549).
|
|
28170
|
+
* Mirrors {@link applyRedshiftDestinationUpdate}. Elasticsearch is the
|
|
28171
|
+
* legacy variant — AWS still accepts it for stacks that pre-date the
|
|
28172
|
+
* Amazonopensearchservice rename.
|
|
28173
|
+
*/
|
|
28174
|
+
async applyElasticsearchDestinationUpdate(physicalId, nextConfig) {
|
|
28175
|
+
const desc = (await this.getClient().send(new DescribeDeliveryStreamCommand({ DeliveryStreamName: physicalId }))).DeliveryStreamDescription;
|
|
28176
|
+
const currentVersionId = desc?.VersionId;
|
|
28177
|
+
const destinationId = desc?.Destinations?.[0]?.DestinationId;
|
|
28178
|
+
if (!currentVersionId || !destinationId) throw new ProvisioningError(`DescribeDeliveryStream for ${physicalId} did not return VersionId or DestinationId; UpdateDestination cannot proceed.`, "AWS::KinesisFirehose::DeliveryStream", physicalId);
|
|
28179
|
+
await this.getClient().send(new UpdateDestinationCommand({
|
|
28180
|
+
DeliveryStreamName: physicalId,
|
|
28181
|
+
CurrentDeliveryStreamVersionId: currentVersionId,
|
|
28182
|
+
DestinationId: destinationId,
|
|
28183
|
+
ElasticsearchDestinationUpdate: this.mapElasticsearchConfigToUpdate(nextConfig)
|
|
28184
|
+
}));
|
|
28185
|
+
}
|
|
28186
|
+
/**
|
|
28187
|
+
* Map CFn `ElasticsearchDestinationConfiguration` to the
|
|
28188
|
+
* `ElasticsearchDestinationUpdate` shape (#549). Every field
|
|
28189
|
+
* `!== undefined` gated. CFn `S3Configuration` → SDK `S3Update`.
|
|
28190
|
+
* Field set mirrors {@link mapAmazonopensearchserviceConfigToUpdate}
|
|
28191
|
+
* — Elasticsearch is the legacy variant of the same destination.
|
|
28192
|
+
*/
|
|
28193
|
+
mapElasticsearchConfigToUpdate(config) {
|
|
28194
|
+
const result = {};
|
|
28195
|
+
const roleArn = config["RoleARN"] ?? config["RoleArn"];
|
|
28196
|
+
if (roleArn !== void 0) result.RoleARN = roleArn;
|
|
28197
|
+
const domainArn = config["DomainARN"] ?? config["DomainArn"];
|
|
28198
|
+
if (domainArn !== void 0) result.DomainARN = domainArn;
|
|
28199
|
+
if (config["ClusterEndpoint"] !== void 0) result.ClusterEndpoint = config["ClusterEndpoint"];
|
|
28200
|
+
if (config["IndexName"] !== void 0) result.IndexName = config["IndexName"];
|
|
28201
|
+
if (config["TypeName"] !== void 0) result.TypeName = config["TypeName"];
|
|
28202
|
+
if (config["IndexRotationPeriod"] !== void 0) result.IndexRotationPeriod = config["IndexRotationPeriod"];
|
|
28203
|
+
if (config["BufferingHints"] !== void 0) result.BufferingHints = config["BufferingHints"];
|
|
28204
|
+
if (config["RetryOptions"] !== void 0) result.RetryOptions = config["RetryOptions"];
|
|
28205
|
+
if (config["S3Configuration"] !== void 0) result.S3Update = this.mapS3ConfigToUpdate(config["S3Configuration"]);
|
|
28206
|
+
if (config["ProcessingConfiguration"] !== void 0) result.ProcessingConfiguration = config["ProcessingConfiguration"];
|
|
28207
|
+
if (config["CloudWatchLoggingOptions"] !== void 0) result.CloudWatchLoggingOptions = config["CloudWatchLoggingOptions"];
|
|
28208
|
+
if (config["DocumentIdOptions"] !== void 0) result.DocumentIdOptions = config["DocumentIdOptions"];
|
|
28209
|
+
return result;
|
|
28210
|
+
}
|
|
28211
|
+
/**
|
|
28212
|
+
* Apply UpdateDestination for `IcebergDestinationConfiguration` (#549).
|
|
28213
|
+
* Mirrors {@link applyRedshiftDestinationUpdate}.
|
|
28214
|
+
*/
|
|
28215
|
+
async applyIcebergDestinationUpdate(physicalId, nextConfig) {
|
|
28216
|
+
const desc = (await this.getClient().send(new DescribeDeliveryStreamCommand({ DeliveryStreamName: physicalId }))).DeliveryStreamDescription;
|
|
28217
|
+
const currentVersionId = desc?.VersionId;
|
|
28218
|
+
const destinationId = desc?.Destinations?.[0]?.DestinationId;
|
|
28219
|
+
if (!currentVersionId || !destinationId) throw new ProvisioningError(`DescribeDeliveryStream for ${physicalId} did not return VersionId or DestinationId; UpdateDestination cannot proceed.`, "AWS::KinesisFirehose::DeliveryStream", physicalId);
|
|
28220
|
+
await this.getClient().send(new UpdateDestinationCommand({
|
|
28221
|
+
DeliveryStreamName: physicalId,
|
|
28222
|
+
CurrentDeliveryStreamVersionId: currentVersionId,
|
|
28223
|
+
DestinationId: destinationId,
|
|
28224
|
+
IcebergDestinationUpdate: this.mapIcebergConfigToUpdate(nextConfig)
|
|
28225
|
+
}));
|
|
28226
|
+
}
|
|
28227
|
+
/**
|
|
28228
|
+
* Map CFn `IcebergDestinationConfiguration` to the
|
|
28229
|
+
* `IcebergDestinationUpdate` shape (#549). Every field
|
|
28230
|
+
* `!== undefined` gated. **Quirk vs every other destination**: the
|
|
28231
|
+
* SDK Update shape's S3 field is `S3Configuration` (a full
|
|
28232
|
+
* `S3DestinationConfiguration`), NOT `S3Update`. Iceberg is a newer
|
|
28233
|
+
* destination type and AWS chose to keep the field name aligned with
|
|
28234
|
+
* the create shape; this reverse-mapper forwards it verbatim.
|
|
28235
|
+
*/
|
|
28236
|
+
mapIcebergConfigToUpdate(config) {
|
|
28237
|
+
const result = {};
|
|
28238
|
+
if (config["DestinationTableConfigurationList"] !== void 0) result.DestinationTableConfigurationList = config["DestinationTableConfigurationList"];
|
|
28239
|
+
if (config["SchemaEvolutionConfiguration"] !== void 0) result.SchemaEvolutionConfiguration = config["SchemaEvolutionConfiguration"];
|
|
28240
|
+
if (config["TableCreationConfiguration"] !== void 0) result.TableCreationConfiguration = config["TableCreationConfiguration"];
|
|
28241
|
+
if (config["BufferingHints"] !== void 0) result.BufferingHints = config["BufferingHints"];
|
|
28242
|
+
if (config["CloudWatchLoggingOptions"] !== void 0) result.CloudWatchLoggingOptions = config["CloudWatchLoggingOptions"];
|
|
28243
|
+
if (config["ProcessingConfiguration"] !== void 0) result.ProcessingConfiguration = config["ProcessingConfiguration"];
|
|
28244
|
+
if (config["S3BackupMode"] !== void 0) result.S3BackupMode = config["S3BackupMode"];
|
|
28245
|
+
if (config["RetryOptions"] !== void 0) result.RetryOptions = config["RetryOptions"];
|
|
28246
|
+
const roleArn = config["RoleARN"] ?? config["RoleArn"];
|
|
28247
|
+
if (roleArn !== void 0) result.RoleARN = roleArn;
|
|
28248
|
+
if (config["AppendOnly"] !== void 0) result.AppendOnly = config["AppendOnly"];
|
|
28249
|
+
if (config["CatalogConfiguration"] !== void 0) result.CatalogConfiguration = config["CatalogConfiguration"];
|
|
28250
|
+
if (config["S3Configuration"] !== void 0) result.S3Configuration = this.mapS3DestinationConfiguration(config["S3Configuration"]);
|
|
28251
|
+
return result;
|
|
28252
|
+
}
|
|
28253
|
+
/**
|
|
28254
|
+
* Apply UpdateDestination for `SnowflakeDestinationConfiguration` (#549).
|
|
28255
|
+
* Mirrors {@link applyRedshiftDestinationUpdate}.
|
|
28256
|
+
*/
|
|
28257
|
+
async applySnowflakeDestinationUpdate(physicalId, nextConfig) {
|
|
28258
|
+
const desc = (await this.getClient().send(new DescribeDeliveryStreamCommand({ DeliveryStreamName: physicalId }))).DeliveryStreamDescription;
|
|
28259
|
+
const currentVersionId = desc?.VersionId;
|
|
28260
|
+
const destinationId = desc?.Destinations?.[0]?.DestinationId;
|
|
28261
|
+
if (!currentVersionId || !destinationId) throw new ProvisioningError(`DescribeDeliveryStream for ${physicalId} did not return VersionId or DestinationId; UpdateDestination cannot proceed.`, "AWS::KinesisFirehose::DeliveryStream", physicalId);
|
|
28262
|
+
await this.getClient().send(new UpdateDestinationCommand({
|
|
28263
|
+
DeliveryStreamName: physicalId,
|
|
28264
|
+
CurrentDeliveryStreamVersionId: currentVersionId,
|
|
28265
|
+
DestinationId: destinationId,
|
|
28266
|
+
SnowflakeDestinationUpdate: this.mapSnowflakeConfigToUpdate(nextConfig)
|
|
28267
|
+
}));
|
|
28268
|
+
}
|
|
28269
|
+
/**
|
|
28270
|
+
* Map CFn `SnowflakeDestinationConfiguration` to the
|
|
28271
|
+
* `SnowflakeDestinationUpdate` shape (#549). Every field
|
|
28272
|
+
* `!== undefined` gated. CFn `S3Configuration` → SDK `S3Update`.
|
|
28273
|
+
* Snowflake carries many connector-side credential / target-table
|
|
28274
|
+
* fields (`AccountUrl` / `PrivateKey` / `KeyPassphrase` / `User` /
|
|
28275
|
+
* `Database` / `Schema` / `Table`) all pass-through.
|
|
28276
|
+
*/
|
|
28277
|
+
mapSnowflakeConfigToUpdate(config) {
|
|
28278
|
+
const result = {};
|
|
28279
|
+
if (config["AccountUrl"] !== void 0) result.AccountUrl = config["AccountUrl"];
|
|
28280
|
+
if (config["PrivateKey"] !== void 0) result.PrivateKey = config["PrivateKey"];
|
|
28281
|
+
if (config["KeyPassphrase"] !== void 0) result.KeyPassphrase = config["KeyPassphrase"];
|
|
28282
|
+
if (config["User"] !== void 0) result.User = config["User"];
|
|
28283
|
+
if (config["Database"] !== void 0) result.Database = config["Database"];
|
|
28284
|
+
if (config["Schema"] !== void 0) result.Schema = config["Schema"];
|
|
28285
|
+
if (config["Table"] !== void 0) result.Table = config["Table"];
|
|
28286
|
+
if (config["SnowflakeRoleConfiguration"] !== void 0) result.SnowflakeRoleConfiguration = config["SnowflakeRoleConfiguration"];
|
|
28287
|
+
if (config["DataLoadingOption"] !== void 0) result.DataLoadingOption = config["DataLoadingOption"];
|
|
28288
|
+
if (config["MetaDataColumnName"] !== void 0) result.MetaDataColumnName = config["MetaDataColumnName"];
|
|
28289
|
+
if (config["ContentColumnName"] !== void 0) result.ContentColumnName = config["ContentColumnName"];
|
|
28290
|
+
if (config["CloudWatchLoggingOptions"] !== void 0) result.CloudWatchLoggingOptions = config["CloudWatchLoggingOptions"];
|
|
28291
|
+
if (config["ProcessingConfiguration"] !== void 0) result.ProcessingConfiguration = config["ProcessingConfiguration"];
|
|
28292
|
+
const roleArn = config["RoleARN"] ?? config["RoleArn"];
|
|
28293
|
+
if (roleArn !== void 0) result.RoleARN = roleArn;
|
|
28294
|
+
if (config["RetryOptions"] !== void 0) result.RetryOptions = config["RetryOptions"];
|
|
28295
|
+
if (config["S3BackupMode"] !== void 0) result.S3BackupMode = config["S3BackupMode"];
|
|
28296
|
+
if (config["S3Configuration"] !== void 0) result.S3Update = this.mapS3ConfigToUpdate(config["S3Configuration"]);
|
|
28297
|
+
if (config["SecretsManagerConfiguration"] !== void 0) result.SecretsManagerConfiguration = config["SecretsManagerConfiguration"];
|
|
28298
|
+
if (config["BufferingHints"] !== void 0) result.BufferingHints = config["BufferingHints"];
|
|
28299
|
+
return result;
|
|
28300
|
+
}
|
|
28301
|
+
/**
|
|
27890
28302
|
* Map CFn `S3DestinationConfiguration` to the `S3DestinationUpdate`
|
|
27891
28303
|
* shape used by AWS `UpdateDestinationCommand` (#477; consumed by
|
|
27892
28304
|
* {@link mapExtendedS3ConfigToUpdate} for the `S3BackupUpdate` field).
|
|
@@ -30944,6 +31356,602 @@ function mapNotificationsToCfn(configurations) {
|
|
|
30944
31356
|
return result;
|
|
30945
31357
|
}
|
|
30946
31358
|
|
|
31359
|
+
//#endregion
|
|
31360
|
+
//#region src/cli/commands/destroy-runner.ts
|
|
31361
|
+
/**
|
|
31362
|
+
* Resource-type → state-property name pairs that gate AWS deletion
|
|
31363
|
+
* protection. Used by the `--remove-protection` confirmation prompt to
|
|
31364
|
+
* report a best-effort count of resources that will have protection
|
|
31365
|
+
* cleared. The actual flip-off is unconditional inside each provider's
|
|
31366
|
+
* `delete()` (idempotent — safe when AWS already has protection off),
|
|
31367
|
+
* so the count is informational only.
|
|
31368
|
+
*
|
|
31369
|
+
* Most types use a boolean flag — the value `true` is what we count.
|
|
31370
|
+
* Two types use a string-valued enum (Cognito UserPool's
|
|
31371
|
+
* `DeletionProtection` is `'ACTIVE' | 'INACTIVE'`, AutoScalingGroup's
|
|
31372
|
+
* `DeletionProtection` is `'none' | 'prevent-force-deletion' |
|
|
31373
|
+
* 'prevent-all-deletion'`). For those, the helper checks against a
|
|
31374
|
+
* per-type set of "active" values via `PROTECTION_ACTIVE_VALUES_BY_TYPE`.
|
|
31375
|
+
*
|
|
31376
|
+
* Exported for unit-test coverage of `countProtectedResources`.
|
|
31377
|
+
*/
|
|
31378
|
+
const PROTECTION_PROPERTY_BY_TYPE = {
|
|
31379
|
+
"AWS::Logs::LogGroup": "DeletionProtectionEnabled",
|
|
31380
|
+
"AWS::RDS::DBInstance": "DeletionProtection",
|
|
31381
|
+
"AWS::RDS::DBCluster": "DeletionProtection",
|
|
31382
|
+
"AWS::DocDB::DBCluster": "DeletionProtection",
|
|
31383
|
+
"AWS::Neptune::DBCluster": "DeletionProtection",
|
|
31384
|
+
"AWS::Neptune::DBInstance": "DeletionProtection",
|
|
31385
|
+
"AWS::DynamoDB::Table": "DeletionProtectionEnabled",
|
|
31386
|
+
"AWS::DynamoDB::GlobalTable": "DeletionProtectionEnabled",
|
|
31387
|
+
"AWS::EC2::Instance": "DisableApiTermination",
|
|
31388
|
+
"AWS::Cognito::UserPool": "DeletionProtection",
|
|
31389
|
+
"AWS::AutoScaling::AutoScalingGroup": "DeletionProtection"
|
|
31390
|
+
};
|
|
31391
|
+
/**
|
|
31392
|
+
* For string-valued protection enums, the set of values that count as
|
|
31393
|
+
* "currently protected". Types absent from this map use the default
|
|
31394
|
+
* (boolean `true`).
|
|
31395
|
+
*/
|
|
31396
|
+
const PROTECTION_ACTIVE_VALUES_BY_TYPE = {
|
|
31397
|
+
"AWS::Cognito::UserPool": new Set(["ACTIVE"]),
|
|
31398
|
+
"AWS::AutoScaling::AutoScalingGroup": new Set(["prevent-force-deletion", "prevent-all-deletion"])
|
|
31399
|
+
};
|
|
31400
|
+
/**
|
|
31401
|
+
* Count how many resources in a stack's recorded state appear to have
|
|
31402
|
+
* deletion protection enabled. Walks `properties` and `observedProperties`
|
|
31403
|
+
* for the property name registered against each resource type in
|
|
31404
|
+
* `PROTECTION_PROPERTY_BY_TYPE`. ELBv2 LoadBalancer protection lives in
|
|
31405
|
+
* `LoadBalancerAttributes` (a CFn `Array<{Key, Value}>`), so it's
|
|
31406
|
+
* handled separately via the `deletion_protection.enabled` key.
|
|
31407
|
+
*/
|
|
31408
|
+
function countProtectedResources(state) {
|
|
31409
|
+
let count = 0;
|
|
31410
|
+
for (const resource of Object.values(state.resources ?? {})) {
|
|
31411
|
+
const propName = PROTECTION_PROPERTY_BY_TYPE[resource.resourceType];
|
|
31412
|
+
if (propName) {
|
|
31413
|
+
const recorded = resource.properties?.[propName] ?? resource.observedProperties?.[propName];
|
|
31414
|
+
const activeValues = PROTECTION_ACTIVE_VALUES_BY_TYPE[resource.resourceType];
|
|
31415
|
+
if (activeValues) {
|
|
31416
|
+
if (activeValues.has(recorded)) count++;
|
|
31417
|
+
} else if (recorded === true) count++;
|
|
31418
|
+
continue;
|
|
31419
|
+
}
|
|
31420
|
+
if (resource.resourceType === "AWS::ElasticLoadBalancingV2::LoadBalancer") {
|
|
31421
|
+
if (((resource.properties?.["LoadBalancerAttributes"] ?? resource.observedProperties?.["LoadBalancerAttributes"])?.find((a) => a?.Key === "deletion_protection.enabled"))?.Value === "true") count++;
|
|
31422
|
+
}
|
|
31423
|
+
}
|
|
31424
|
+
return count;
|
|
31425
|
+
}
|
|
31426
|
+
/**
|
|
31427
|
+
* Run the destroy lifecycle for one stack against an already-loaded
|
|
31428
|
+
* `StackState`, reusing the caller's state backend / lock manager.
|
|
31429
|
+
*
|
|
31430
|
+
* Hoisted from `cdkd destroy` so the new `cdkd state destroy` subcommand
|
|
31431
|
+
* can call into the exact same per-stack pipeline without depending on
|
|
31432
|
+
* synth or the CDK app. The state-source split is the only meaningful
|
|
31433
|
+
* difference between the two commands — everything from "prompt the user"
|
|
31434
|
+
* onwards is identical.
|
|
31435
|
+
*
|
|
31436
|
+
* Side effects:
|
|
31437
|
+
* - Acquires (and releases) the stack's S3 lock.
|
|
31438
|
+
* - Switches `process.env.AWS_REGION` for the duration of the destroy when
|
|
31439
|
+
* the stack's recorded region differs from `baseRegion`. Restored in the
|
|
31440
|
+
* `finally` block.
|
|
31441
|
+
* - On full success, deletes the state file. On any failure, the state
|
|
31442
|
+
* file is preserved so the user can retry.
|
|
31443
|
+
*/
|
|
31444
|
+
async function runDestroyForStack(stackName, state, ctx) {
|
|
31445
|
+
const logger = getLogger();
|
|
31446
|
+
const result = {
|
|
31447
|
+
stackName,
|
|
31448
|
+
cancelled: false,
|
|
31449
|
+
skippedEmpty: false,
|
|
31450
|
+
deletedCount: 0,
|
|
31451
|
+
retainedCount: 0,
|
|
31452
|
+
errorCount: 0
|
|
31453
|
+
};
|
|
31454
|
+
const resourceCount = Object.keys(state.resources).length;
|
|
31455
|
+
const regionForState = state.region ?? ctx.baseRegion;
|
|
31456
|
+
if (resourceCount === 0) {
|
|
31457
|
+
logger.info(`Stack ${stackName} has no resources, cleaning up state...`);
|
|
31458
|
+
await ctx.stateBackend.deleteState(stackName, regionForState);
|
|
31459
|
+
logger.info(`${green("✓")} State deleted`);
|
|
31460
|
+
result.skippedEmpty = true;
|
|
31461
|
+
return result;
|
|
31462
|
+
}
|
|
31463
|
+
const needsStrongRefCheck = !!(state.outputs && Object.keys(state.outputs).length > 0);
|
|
31464
|
+
if (needsStrongRefCheck) {
|
|
31465
|
+
const consumers = await scanActiveConsumers(stackName, regionForState, ctx);
|
|
31466
|
+
if (consumers.length > 0) throw new StackHasActiveImportsError(stackName, regionForState, consumers);
|
|
31467
|
+
}
|
|
31468
|
+
logger.info(`\nResources to be deleted (${resourceCount}):`);
|
|
31469
|
+
for (const [logicalId, resource] of Object.entries(state.resources)) logger.info(` - ${logicalId} (${resource.resourceType})`);
|
|
31470
|
+
const protectedCount = ctx.removeProtection ? countProtectedResources(state) : 0;
|
|
31471
|
+
if (!ctx.skipConfirmation) {
|
|
31472
|
+
const rl = readline.createInterface({
|
|
31473
|
+
input: process.stdin,
|
|
31474
|
+
output: process.stdout
|
|
31475
|
+
});
|
|
31476
|
+
const prompt = ctx.removeProtection ? `\nAbout to destroy ${resourceCount} resources from stack "${stackName}", REMOVING DELETION PROTECTION on ${protectedCount} of them. Continue? (y/N): ` : `\nAre you sure you want to destroy stack "${stackName}" and delete all ${resourceCount} resources? (Y/n): `;
|
|
31477
|
+
const answer = await rl.question(prompt);
|
|
31478
|
+
rl.close();
|
|
31479
|
+
const trimmed = answer.trim().toLowerCase();
|
|
31480
|
+
if (ctx.removeProtection) {
|
|
31481
|
+
if (trimmed !== "y" && trimmed !== "yes") {
|
|
31482
|
+
logger.info("Destroy cancelled");
|
|
31483
|
+
result.cancelled = true;
|
|
31484
|
+
return result;
|
|
31485
|
+
}
|
|
31486
|
+
} else if (trimmed === "n" || trimmed === "no") {
|
|
31487
|
+
logger.info("Destroy cancelled");
|
|
31488
|
+
result.cancelled = true;
|
|
31489
|
+
return result;
|
|
31490
|
+
}
|
|
31491
|
+
}
|
|
31492
|
+
const stackRegion = state.region;
|
|
31493
|
+
let destroyProviderRegistry = ctx.providerRegistry;
|
|
31494
|
+
let destroyAwsClients;
|
|
31495
|
+
if (stackRegion && stackRegion !== ctx.baseRegion) {
|
|
31496
|
+
logger.info(`Stack region: ${stackRegion}`);
|
|
31497
|
+
process.env["AWS_REGION"] = stackRegion;
|
|
31498
|
+
process.env["AWS_DEFAULT_REGION"] = stackRegion;
|
|
31499
|
+
destroyAwsClients = new AwsClients({
|
|
31500
|
+
region: stackRegion,
|
|
31501
|
+
...ctx.profile && { profile: ctx.profile }
|
|
31502
|
+
});
|
|
31503
|
+
setAwsClients(destroyAwsClients);
|
|
31504
|
+
destroyProviderRegistry = new ProviderRegistry();
|
|
31505
|
+
registerAllProviders(destroyProviderRegistry);
|
|
31506
|
+
destroyProviderRegistry.setCustomResourceResponseBucket(ctx.stateBucket);
|
|
31507
|
+
}
|
|
31508
|
+
logger.info(`\nAcquiring lock for stack ${stackName}...`);
|
|
31509
|
+
await ctx.lockManager.acquireLock(stackName, regionForState, void 0, "destroy");
|
|
31510
|
+
if (needsStrongRefCheck) {
|
|
31511
|
+
const consumers = await scanActiveConsumers(stackName, regionForState, ctx);
|
|
31512
|
+
if (consumers.length > 0) {
|
|
31513
|
+
try {
|
|
31514
|
+
await ctx.lockManager.releaseLock(stackName, regionForState);
|
|
31515
|
+
} catch (releaseErr) {
|
|
31516
|
+
logger.warn(`Failed to release lock after strong-ref refusal: ${releaseErr instanceof Error ? releaseErr.message : String(releaseErr)}`);
|
|
31517
|
+
}
|
|
31518
|
+
throw new StackHasActiveImportsError(stackName, regionForState, consumers);
|
|
31519
|
+
}
|
|
31520
|
+
}
|
|
31521
|
+
const renderer = getLiveRenderer();
|
|
31522
|
+
renderer.start();
|
|
31523
|
+
try {
|
|
31524
|
+
logger.info("Building dependency graph...");
|
|
31525
|
+
const template = {
|
|
31526
|
+
AWSTemplateFormatVersion: "2010-09-09",
|
|
31527
|
+
Resources: {}
|
|
31528
|
+
};
|
|
31529
|
+
for (const [logicalId, resource] of Object.entries(state.resources)) template.Resources[logicalId] = {
|
|
31530
|
+
Type: resource.resourceType,
|
|
31531
|
+
Properties: resource.properties || {},
|
|
31532
|
+
...resource.dependencies && resource.dependencies.length > 0 && { DependsOn: resource.dependencies }
|
|
31533
|
+
};
|
|
31534
|
+
const typeToLogicalIds = /* @__PURE__ */ new Map();
|
|
31535
|
+
for (const [logicalId, resource] of Object.entries(state.resources)) {
|
|
31536
|
+
const ids = typeToLogicalIds.get(resource.resourceType) ?? [];
|
|
31537
|
+
ids.push(logicalId);
|
|
31538
|
+
typeToLogicalIds.set(resource.resourceType, ids);
|
|
31539
|
+
}
|
|
31540
|
+
for (const [logicalId, resource] of Object.entries(state.resources)) {
|
|
31541
|
+
const mustDeleteAfter = IMPLICIT_DELETE_DEPENDENCIES[resource.resourceType];
|
|
31542
|
+
if (!mustDeleteAfter) continue;
|
|
31543
|
+
for (const depType of mustDeleteAfter) {
|
|
31544
|
+
const depIds = typeToLogicalIds.get(depType);
|
|
31545
|
+
if (!depIds) continue;
|
|
31546
|
+
for (const depId of depIds) {
|
|
31547
|
+
const existing = template.Resources[depId]?.DependsOn ?? [];
|
|
31548
|
+
const depsArray = Array.isArray(existing) ? existing : [existing];
|
|
31549
|
+
if (!depsArray.includes(logicalId)) {
|
|
31550
|
+
template.Resources[depId] = {
|
|
31551
|
+
...template.Resources[depId],
|
|
31552
|
+
DependsOn: [...depsArray, logicalId]
|
|
31553
|
+
};
|
|
31554
|
+
logger.debug(`Implicit delete dependency: ${depId} (${depType}) must be deleted before ${logicalId} (${resource.resourceType})`);
|
|
31555
|
+
}
|
|
31556
|
+
}
|
|
31557
|
+
}
|
|
31558
|
+
}
|
|
31559
|
+
const dagBuilder = new DagBuilder();
|
|
31560
|
+
const graph = dagBuilder.buildGraph(template);
|
|
31561
|
+
const executionLevels = dagBuilder.getExecutionLevels(graph);
|
|
31562
|
+
logger.debug(`Dependency graph: ${executionLevels.length} level(s)`);
|
|
31563
|
+
for (let levelIndex = executionLevels.length - 1; levelIndex >= 0; levelIndex--) {
|
|
31564
|
+
const level = executionLevels[levelIndex];
|
|
31565
|
+
if (!level) continue;
|
|
31566
|
+
logger.debug(`Deletion level ${executionLevels.length - levelIndex}/${executionLevels.length} (${level.length} resources)`);
|
|
31567
|
+
const stackRegion = state.region ?? ctx.baseRegion;
|
|
31568
|
+
const deletePromises = level.map(async (logicalId) => {
|
|
31569
|
+
const resource = state.resources[logicalId];
|
|
31570
|
+
if (!resource) {
|
|
31571
|
+
logger.warn(`Resource ${logicalId} not found in state, skipping`);
|
|
31572
|
+
return;
|
|
31573
|
+
}
|
|
31574
|
+
if (shouldRetainResource(resource.deletionPolicy)) {
|
|
31575
|
+
logger.info(` ⊘ ${logicalId} (${resource.resourceType}) retained — DeletionPolicy: ${resource.deletionPolicy}`);
|
|
31576
|
+
result.retainedCount++;
|
|
31577
|
+
return;
|
|
31578
|
+
}
|
|
31579
|
+
const baseLabel = `Deleting ${logicalId} (${resource.resourceType})`;
|
|
31580
|
+
renderer.addTask(logicalId, baseLabel);
|
|
31581
|
+
try {
|
|
31582
|
+
const provider = destroyProviderRegistry.getProvider(resource.resourceType);
|
|
31583
|
+
const providerMinTimeoutMs = provider.getMinResourceTimeoutMs?.() ?? 0;
|
|
31584
|
+
const warnAfterMs = ctx.resourceWarnAfterByType?.[resource.resourceType] ?? ctx.resourceWarnAfterMs ?? 3e5;
|
|
31585
|
+
const globalTimeoutMs = ctx.resourceTimeoutMs ?? 18e5;
|
|
31586
|
+
const timeoutMs = ctx.resourceTimeoutByType?.[resource.resourceType] ?? Math.max(providerMinTimeoutMs, globalTimeoutMs);
|
|
31587
|
+
await withResourceDeadline(async () => {
|
|
31588
|
+
const maxAttempts = provider.disableOuterRetry ? 0 : 3;
|
|
31589
|
+
let lastDeleteError;
|
|
31590
|
+
for (let attempt = 0; attempt <= maxAttempts; attempt++) try {
|
|
31591
|
+
await provider.delete(logicalId, resource.physicalId, resource.resourceType, resource.properties, {
|
|
31592
|
+
...state.region !== void 0 && { expectedRegion: state.region },
|
|
31593
|
+
...ctx.removeProtection === true && { removeProtection: true }
|
|
31594
|
+
});
|
|
31595
|
+
lastDeleteError = null;
|
|
31596
|
+
break;
|
|
31597
|
+
} catch (retryError) {
|
|
31598
|
+
lastDeleteError = retryError;
|
|
31599
|
+
const msg = retryError instanceof Error ? retryError.message : String(retryError);
|
|
31600
|
+
if (!(msg.includes("Too Many Requests") || msg.includes("has dependencies") || msg.includes("can't be deleted since") || msg.includes("DependencyViolation")) || attempt >= maxAttempts) break;
|
|
31601
|
+
const delay = 5e3 * Math.pow(2, attempt);
|
|
31602
|
+
logger.debug(` ⏳ Retrying delete ${logicalId} in ${delay / 1e3}s (attempt ${attempt + 1}/${maxAttempts})`);
|
|
31603
|
+
await new Promise((resolve) => setTimeout(resolve, delay));
|
|
31604
|
+
}
|
|
31605
|
+
if (lastDeleteError) throw lastDeleteError;
|
|
31606
|
+
}, {
|
|
31607
|
+
warnAfterMs,
|
|
31608
|
+
timeoutMs,
|
|
31609
|
+
onWarn: (elapsedMs) => {
|
|
31610
|
+
const minutes = Math.max(1, Math.round(elapsedMs / 6e4));
|
|
31611
|
+
renderer.updateTaskLabel(logicalId, `${baseLabel} [taking longer than expected, ${minutes}m+]`);
|
|
31612
|
+
renderer.printAbove(() => {
|
|
31613
|
+
logger.warn(`${logicalId} (${resource.resourceType}) has been deleting for ${minutes}m — still waiting`);
|
|
31614
|
+
});
|
|
31615
|
+
},
|
|
31616
|
+
onTimeout: (elapsedMs) => new ResourceTimeoutError(logicalId, resource.resourceType, stackRegion, elapsedMs, "DELETE", timeoutMs)
|
|
31617
|
+
});
|
|
31618
|
+
renderer.removeTask(logicalId);
|
|
31619
|
+
logger.info(` ${red("✗")} ${bold(logicalId)} ${gray(`(${resource.resourceType})`)} ${red("deleted")}`);
|
|
31620
|
+
result.deletedCount++;
|
|
31621
|
+
} catch (error) {
|
|
31622
|
+
renderer.removeTask(logicalId);
|
|
31623
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
31624
|
+
if (msg.includes("does not exist") || msg.includes("not found") || msg.includes("No policy found") || msg.includes("NoSuchEntity") || msg.includes("NotFoundException")) {
|
|
31625
|
+
logger.debug(` ${logicalId} already deleted, removing from state`);
|
|
31626
|
+
result.deletedCount++;
|
|
31627
|
+
} else if (error instanceof ResourceTimeoutError) {
|
|
31628
|
+
const wrapped = new ProvisioningError(error.message, resource.resourceType, logicalId, resource.physicalId, error);
|
|
31629
|
+
logger.error(` ✗ Failed to delete ${logicalId}:`, wrapped.message);
|
|
31630
|
+
result.errorCount++;
|
|
31631
|
+
} else {
|
|
31632
|
+
logger.error(` ✗ Failed to delete ${logicalId}:`, String(error));
|
|
31633
|
+
result.errorCount++;
|
|
31634
|
+
}
|
|
31635
|
+
} finally {
|
|
31636
|
+
renderer.removeTask(logicalId);
|
|
31637
|
+
}
|
|
31638
|
+
});
|
|
31639
|
+
await Promise.all(deletePromises);
|
|
31640
|
+
}
|
|
31641
|
+
if (result.errorCount === 0) {
|
|
31642
|
+
await ctx.stateBackend.deleteState(stackName, regionForState);
|
|
31643
|
+
logger.debug("State deleted");
|
|
31644
|
+
if (ctx.exportIndexStore) await ctx.exportIndexStore.removeStack(stackName, regionForState);
|
|
31645
|
+
} else logger.warn(`${result.errorCount} resource(s) failed to delete. State preserved.`);
|
|
31646
|
+
const retainedSuffix = result.retainedCount > 0 ? `, ${result.retainedCount} retained` : "";
|
|
31647
|
+
if (result.errorCount === 0) logger.info(`\n${green("✓")} ${bold(`Stack ${stackName} destroyed`)} (${green(result.deletedCount)} deleted${retainedSuffix}, ${result.errorCount} errors)`);
|
|
31648
|
+
else logger.warn(`\n${yellow("⚠")} ${bold(`Stack ${stackName} partially destroyed`)} (${green(result.deletedCount)} deleted${retainedSuffix}, ${red(result.errorCount)} errors). State preserved — re-run 'cdkd destroy' / 'cdkd state destroy' to clean up.`);
|
|
31649
|
+
} finally {
|
|
31650
|
+
renderer.stop();
|
|
31651
|
+
logger.debug("Releasing lock...");
|
|
31652
|
+
await ctx.lockManager.releaseLock(stackName, regionForState);
|
|
31653
|
+
if (destroyAwsClients) {
|
|
31654
|
+
destroyAwsClients.destroy();
|
|
31655
|
+
process.env["AWS_REGION"] = ctx.baseRegion;
|
|
31656
|
+
process.env["AWS_DEFAULT_REGION"] = ctx.baseRegion;
|
|
31657
|
+
setAwsClients(ctx.baseAwsClients);
|
|
31658
|
+
}
|
|
31659
|
+
}
|
|
31660
|
+
return result;
|
|
31661
|
+
}
|
|
31662
|
+
/**
|
|
31663
|
+
* Strong-reference scan: read every other stack's state.json from the
|
|
31664
|
+
* state bucket and check whether any of its `imports[]` entries names
|
|
31665
|
+
* `producerStack`. Returns the list of offending consumers (possibly
|
|
31666
|
+
* empty).
|
|
31667
|
+
*
|
|
31668
|
+
* NEVER trusts the persistent exports index — a stale index could miss
|
|
31669
|
+
* a freshly-recorded consumer and let a destructive destroy through.
|
|
31670
|
+
* The cost is one `listStacks` + N parallel GETs at destroy time only
|
|
31671
|
+
* (not the deploy hot path), which the user-facing UX rationalizes as
|
|
31672
|
+
* the "destroy is slow OK" trade-off (Issue #343).
|
|
31673
|
+
*/
|
|
31674
|
+
async function scanActiveConsumers(producerStack, producerRegion, ctx) {
|
|
31675
|
+
const refs = await ctx.stateBackend.listStacks();
|
|
31676
|
+
return (await Promise.all(refs.map(async (ref) => {
|
|
31677
|
+
const region = ref.region ?? ctx.baseRegion;
|
|
31678
|
+
if (ref.stackName === producerStack && region === producerRegion) return null;
|
|
31679
|
+
try {
|
|
31680
|
+
const imports = (await ctx.stateBackend.getState(ref.stackName, region))?.state.imports;
|
|
31681
|
+
if (!imports || imports.length === 0) return null;
|
|
31682
|
+
const matches = imports.filter((entry) => entry.sourceStack === producerStack && entry.sourceRegion === producerRegion);
|
|
31683
|
+
if (matches.length === 0) return null;
|
|
31684
|
+
return matches.map((entry) => ({
|
|
31685
|
+
consumerStack: ref.stackName,
|
|
31686
|
+
consumerRegion: region,
|
|
31687
|
+
exportName: entry.exportName
|
|
31688
|
+
}));
|
|
31689
|
+
} catch {
|
|
31690
|
+
return null;
|
|
31691
|
+
}
|
|
31692
|
+
}))).filter((r) => r !== null).flat();
|
|
31693
|
+
}
|
|
31694
|
+
|
|
31695
|
+
//#endregion
|
|
31696
|
+
//#region src/provisioning/nested-stack-context.ts
|
|
31697
|
+
const storage = new AsyncLocalStorage();
|
|
31698
|
+
/**
|
|
31699
|
+
* Run `fn` inside a NestedStackProvider context scope. Calls to
|
|
31700
|
+
* `getCurrentNestedStackContext()` from inside `fn` (and any awaited callees)
|
|
31701
|
+
* return `ctx`. Nested scopes shadow outer ones — the recursive provider
|
|
31702
|
+
* uses this to switch the "current parent" to the child before kicking off
|
|
31703
|
+
* the child's deploy / destroy, so grand-nested handling resolves against
|
|
31704
|
+
* the right parent.
|
|
31705
|
+
*/
|
|
31706
|
+
function withNestedStackContext(ctx, fn) {
|
|
31707
|
+
return storage.run(ctx, fn);
|
|
31708
|
+
}
|
|
31709
|
+
/**
|
|
31710
|
+
* Returns the current `NestedStackProviderContext`, or `undefined` when called
|
|
31711
|
+
* outside any `withNestedStackContext` scope (= cdkd is operating on a
|
|
31712
|
+
* top-level stack with no nested-stack work in flight).
|
|
31713
|
+
*
|
|
31714
|
+
* `NestedStackProvider.create / update / delete` MUST find a context here —
|
|
31715
|
+
* absence means a caller forgot to wrap the deploy / destroy entry point.
|
|
31716
|
+
*/
|
|
31717
|
+
function getCurrentNestedStackContext() {
|
|
31718
|
+
return storage.getStore();
|
|
31719
|
+
}
|
|
31720
|
+
|
|
31721
|
+
//#endregion
|
|
31722
|
+
//#region src/provisioning/providers/nested-stack-provider.ts
|
|
31723
|
+
/**
|
|
31724
|
+
* Provider for `AWS::CloudFormation::Stack` — cdkd's recursive nested-stack
|
|
31725
|
+
* adapter. Issue [#459](https://github.com/go-to-k/cdkd/issues/459); see
|
|
31726
|
+
* [docs/design/459-nested-stacks.md](../../../docs/design/459-nested-stacks.md)
|
|
31727
|
+
* for the full design.
|
|
31728
|
+
*
|
|
31729
|
+
* On `create` / `update`, the provider builds a child {@link DeployEngine}
|
|
31730
|
+
* against the same shared state backend / lock manager / provider registry,
|
|
31731
|
+
* deploys the child template recursively, and surfaces the child's outputs
|
|
31732
|
+
* as `attributes['Outputs.<Key>']` so the parent's
|
|
31733
|
+
* `Fn::GetAtt: [<NestedStack>, 'Outputs.<Key>']` references resolve via
|
|
31734
|
+
* the existing flat-key fast path in {@link IntrinsicFunctionResolver}.
|
|
31735
|
+
*
|
|
31736
|
+
* On `delete`, the provider loads the child's state and routes it through
|
|
31737
|
+
* {@link runDestroyForStack} for a regular reverse-DAG destroy — the same
|
|
31738
|
+
* code `cdkd destroy` uses on a top-level stack.
|
|
31739
|
+
*
|
|
31740
|
+
* The child's state file lives at
|
|
31741
|
+
* `cdkd/<parentStackName>~<NestedStackLogicalId>/<region>/state.json`
|
|
31742
|
+
* (the `~` separator is rare in CDK logical ids; verified safe against
|
|
31743
|
+
* CDK Stage paths which use `/`). The synthesized `physicalId` is a fake
|
|
31744
|
+
* ARN with `cdkd-local` partition so any downstream consumer that
|
|
31745
|
+
* accidentally uses it as a real AWS ARN fails loudly.
|
|
31746
|
+
*/
|
|
31747
|
+
var NestedStackProvider = class {
|
|
31748
|
+
logger = getLogger().child("NestedStackProvider");
|
|
31749
|
+
/**
|
|
31750
|
+
* Opt out of the deploy engine's outer transient-error retry loop. A
|
|
31751
|
+
* nested-stack `create` recursively spawns a child {@link DeployEngine}
|
|
31752
|
+
* that has its own retry / rollback machinery; an outer retry would
|
|
31753
|
+
* re-enter the entire child deploy on a transient error and produce
|
|
31754
|
+
* duplicate AWS resources before the second attempt's per-resource
|
|
31755
|
+
* state save settles. The child engine handles transient errors
|
|
31756
|
+
* internally — mirroring the same opt-out the Custom Resource provider
|
|
31757
|
+
* uses for the same reason.
|
|
31758
|
+
*/
|
|
31759
|
+
disableOuterRetry = true;
|
|
31760
|
+
/**
|
|
31761
|
+
* The CC API fallback path for `AWS::CloudFormation::Stack` would call
|
|
31762
|
+
* CloudFormation's own `CreateStack` — defeating cdkd's whole "no CFn"
|
|
31763
|
+
* approach for the nested children. Refuse the fallback so any future
|
|
31764
|
+
* regression that drops a real property from `handledProperties`
|
|
31765
|
+
* surfaces as an explicit "unhandled property" deploy error instead of
|
|
31766
|
+
* silently round-tripping through CloudFormation.
|
|
31767
|
+
*/
|
|
31768
|
+
disableCcApiFallback = true;
|
|
31769
|
+
/**
|
|
31770
|
+
* Properties this provider actually wires through to the child deploy.
|
|
31771
|
+
* `TemplateURL` is the asset-published S3 URL of the child template
|
|
31772
|
+
* (cdkd reads the local template file via `Metadata['aws:asset:path']`,
|
|
31773
|
+
* so the URL itself is informational here); `Parameters` is the typed
|
|
31774
|
+
* parameter map forwarded as `DeployEngineOptions.parameters` to the
|
|
31775
|
+
* child engine.
|
|
31776
|
+
*/
|
|
31777
|
+
handledProperties = new Map([["AWS::CloudFormation::Stack", new Set(["TemplateURL", "Parameters"])]]);
|
|
31778
|
+
/**
|
|
31779
|
+
* Every other property on `AWS::CloudFormation::Stack` is intentionally
|
|
31780
|
+
* not threaded through — cdkd does not go through CloudFormation, so
|
|
31781
|
+
* CFn-only inputs (rollback / capability / role / notification /
|
|
31782
|
+
* termination-protection / stack-update policy / per-stack timeout /
|
|
31783
|
+
* tags) have no equivalent. The synthesized `Ref` ARN is a placeholder,
|
|
31784
|
+
* not a real AWS resource — so `Tags` and `Description` similarly
|
|
31785
|
+
* have nothing to attach to. `StackName` is replaced by cdkd's derived
|
|
31786
|
+
* `<parent>~<logicalId>` key per design §3, and `TemplateBody` is
|
|
31787
|
+
* superseded by the local `Metadata['aws:asset:path']` lookup.
|
|
31788
|
+
*/
|
|
31789
|
+
unhandledByDesign = new Map([["AWS::CloudFormation::Stack", new Map([
|
|
31790
|
+
["TemplateBody", "CFn-only inline template — cdkd reads the child template from the synth output via Metadata['aws:asset:path'] instead of accepting it inline"],
|
|
31791
|
+
["Capabilities", "CFn-only IAM capability declaration — cdkd does not go through CloudFormation so capabilities have no equivalent"],
|
|
31792
|
+
["Description", "CFn-only informational — no semantic effect on the recursive deploy"],
|
|
31793
|
+
["DisableRollback", "CFn-only — cdkd controls rollback via the top-level deploy-engine --no-rollback flag, not per nested stack"],
|
|
31794
|
+
["EnableTerminationProtection", "CFn-only per-nested-stack flag — cdkd records stack-level terminationProtection at CDK synth time (parent only) and `cdkd destroy` consults that for refusal"],
|
|
31795
|
+
["NotificationARNs", "CFn-only SNS-on-stack-event surface — cdkd has no equivalent (issue #459 design §9)"],
|
|
31796
|
+
["RoleARN", "CFn-only role-assumption — cdkd uses the caller credentials directly, no per-resource role assumption"],
|
|
31797
|
+
["StackName", "cdkd derives the child stack name as `<parent>~<logicalId>` per design §3 (state-key uniqueness); a user-provided StackName has no effect"],
|
|
31798
|
+
["StackPolicyBody", "CFn-only stack-update policy — cdkd has no equivalent (per-resource diff replaces stack-level policy)"],
|
|
31799
|
+
["StackPolicyURL", "CFn-only stack-update policy URL — cdkd has no equivalent"],
|
|
31800
|
+
["StackStatusReason", "CFn-only read-only output — never a real input property"],
|
|
31801
|
+
["Tags", "CFn-only — cdkd does not tag the synthesized \"stack\" (the parent's synthesized ARN is a cdkd-local placeholder, not a real AWS resource)"],
|
|
31802
|
+
["TimeoutInMinutes", "CFn-only stack-create deadline — cdkd uses per-resource --resource-timeout instead (issue #459 design §9)"]
|
|
31803
|
+
])]]);
|
|
31804
|
+
async create(logicalId, _resourceType, properties) {
|
|
31805
|
+
const ctx = this.requireContext();
|
|
31806
|
+
this.requireDeployContext(ctx, "create");
|
|
31807
|
+
const childTemplatePath = ctx.nestedTemplates[logicalId];
|
|
31808
|
+
if (!childTemplatePath) throw new Error(`Nested template file not found for AWS::CloudFormation::Stack '${logicalId}' under parent '${ctx.parentStackName}'. Verify the synth output emits Metadata['aws:asset:path'] on this resource (CDK 2.x cdk.NestedStack does so by default).`);
|
|
31809
|
+
const childTemplate = this.readChildTemplate(childTemplatePath);
|
|
31810
|
+
const childStackName = this.deriveChildStackName(ctx.parentStackName, logicalId);
|
|
31811
|
+
const childRegion = ctx.parentRegion;
|
|
31812
|
+
const childParameters = this.extractParameters(properties);
|
|
31813
|
+
const grandchildTemplates = this.indexGrandchildTemplates(childTemplate, childTemplatePath);
|
|
31814
|
+
const resourceCount = Object.keys(childTemplate.Resources ?? {}).length;
|
|
31815
|
+
this.logger.info(`Deploying nested stack ${childStackName} (logicalId=${logicalId}, ${resourceCount} resource(s))`);
|
|
31816
|
+
await this.runChildDeploy(ctx, logicalId, childStackName, childRegion, childTemplate, childParameters, grandchildTemplates);
|
|
31817
|
+
const attributes = await this.readChildOutputsAsAttributes(ctx, childStackName, childRegion);
|
|
31818
|
+
return {
|
|
31819
|
+
physicalId: this.synthesizeArn(ctx.accountId, ctx.parentRegion, ctx.parentStackName, logicalId),
|
|
31820
|
+
attributes
|
|
31821
|
+
};
|
|
31822
|
+
}
|
|
31823
|
+
async update(logicalId, physicalId, _resourceType, properties, _previousProperties) {
|
|
31824
|
+
const ctx = this.requireContext();
|
|
31825
|
+
this.requireDeployContext(ctx, "update");
|
|
31826
|
+
const childTemplatePath = ctx.nestedTemplates[logicalId];
|
|
31827
|
+
if (!childTemplatePath) throw new Error(`Nested template file not found for AWS::CloudFormation::Stack '${logicalId}' on update.`);
|
|
31828
|
+
const childTemplate = this.readChildTemplate(childTemplatePath);
|
|
31829
|
+
const childStackName = this.deriveChildStackName(ctx.parentStackName, logicalId);
|
|
31830
|
+
const childRegion = ctx.parentRegion;
|
|
31831
|
+
const childParameters = this.extractParameters(properties);
|
|
31832
|
+
const grandchildTemplates = this.indexGrandchildTemplates(childTemplate, childTemplatePath);
|
|
31833
|
+
const resourceCount = Object.keys(childTemplate.Resources ?? {}).length;
|
|
31834
|
+
this.logger.info(`Updating nested stack ${childStackName} (logicalId=${logicalId}, ${resourceCount} resource(s))`);
|
|
31835
|
+
await this.runChildDeploy(ctx, logicalId, childStackName, childRegion, childTemplate, childParameters, grandchildTemplates);
|
|
31836
|
+
return {
|
|
31837
|
+
physicalId,
|
|
31838
|
+
wasReplaced: false,
|
|
31839
|
+
attributes: await this.readChildOutputsAsAttributes(ctx, childStackName, childRegion)
|
|
31840
|
+
};
|
|
31841
|
+
}
|
|
31842
|
+
async delete(logicalId, _physicalId, _resourceType, _properties, deleteContext) {
|
|
31843
|
+
const ctx = this.requireContext();
|
|
31844
|
+
const childStackName = this.deriveChildStackName(ctx.parentStackName, logicalId);
|
|
31845
|
+
const childRegion = ctx.parentRegion;
|
|
31846
|
+
const childStateData = await ctx.stateBackend.getState(childStackName, childRegion);
|
|
31847
|
+
if (!childStateData) {
|
|
31848
|
+
this.logger.debug(`Nested stack ${childStackName} has no state — treating delete as idempotent success.`);
|
|
31849
|
+
return;
|
|
31850
|
+
}
|
|
31851
|
+
const resourceCount = Object.keys(childStateData.state.resources).length;
|
|
31852
|
+
this.logger.info(`Destroying nested stack ${childStackName} (logicalId=${logicalId}, ${resourceCount} resource(s))`);
|
|
31853
|
+
await withNestedStackContext({
|
|
31854
|
+
...ctx,
|
|
31855
|
+
parentStackName: childStackName,
|
|
31856
|
+
parentRegion: childRegion,
|
|
31857
|
+
nestedTemplates: void 0
|
|
31858
|
+
}, () => runDestroyForStack(childStackName, childStateData.state, {
|
|
31859
|
+
stateBackend: ctx.stateBackend,
|
|
31860
|
+
lockManager: ctx.lockManager,
|
|
31861
|
+
providerRegistry: ctx.providerRegistry,
|
|
31862
|
+
baseAwsClients: ctx.awsClients,
|
|
31863
|
+
baseRegion: childRegion,
|
|
31864
|
+
stateBucket: ctx.stateBucket,
|
|
31865
|
+
skipConfirmation: true,
|
|
31866
|
+
...ctx.exportIndexStore && { exportIndexStore: ctx.exportIndexStore },
|
|
31867
|
+
...ctx.destroyOptions?.profile && { profile: ctx.destroyOptions.profile },
|
|
31868
|
+
...deleteContext?.removeProtection === true && { removeProtection: true },
|
|
31869
|
+
...ctx.destroyOptions?.resourceWarnAfterMs !== void 0 && { resourceWarnAfterMs: ctx.destroyOptions.resourceWarnAfterMs },
|
|
31870
|
+
...ctx.destroyOptions?.resourceTimeoutMs !== void 0 && { resourceTimeoutMs: ctx.destroyOptions.resourceTimeoutMs },
|
|
31871
|
+
...ctx.destroyOptions?.resourceWarnAfterByType && { resourceWarnAfterByType: ctx.destroyOptions.resourceWarnAfterByType },
|
|
31872
|
+
...ctx.destroyOptions?.resourceTimeoutByType && { resourceTimeoutByType: ctx.destroyOptions.resourceTimeoutByType }
|
|
31873
|
+
}));
|
|
31874
|
+
}
|
|
31875
|
+
async getAttribute(_physicalId, _resourceType, attributeName) {
|
|
31876
|
+
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.`);
|
|
31877
|
+
}
|
|
31878
|
+
async runChildDeploy(parentCtx, logicalId, childStackName, childRegion, childTemplate, childParameters, grandchildTemplates) {
|
|
31879
|
+
const childEngine = new DeployEngine(parentCtx.stateBackend, parentCtx.lockManager, parentCtx.dagBuilder, parentCtx.diffCalculator, parentCtx.providerRegistry, {
|
|
31880
|
+
...parentCtx.options ?? {},
|
|
31881
|
+
parameters: childParameters,
|
|
31882
|
+
parentStackInfo: {
|
|
31883
|
+
parentStack: parentCtx.parentStackName,
|
|
31884
|
+
parentLogicalId: logicalId,
|
|
31885
|
+
parentRegion: parentCtx.parentRegion
|
|
31886
|
+
}
|
|
31887
|
+
}, childRegion, parentCtx.exportIndexStore);
|
|
31888
|
+
await withNestedStackContext({
|
|
31889
|
+
...parentCtx,
|
|
31890
|
+
parentStackName: childStackName,
|
|
31891
|
+
parentRegion: childRegion,
|
|
31892
|
+
nestedTemplates: grandchildTemplates
|
|
31893
|
+
}, () => childEngine.deploy(childStackName, childTemplate));
|
|
31894
|
+
}
|
|
31895
|
+
async readChildOutputsAsAttributes(ctx, childStackName, childRegion) {
|
|
31896
|
+
const childStateData = await ctx.stateBackend.getState(childStackName, childRegion);
|
|
31897
|
+
if (!childStateData) throw new Error(`Child stack state '${childStackName}' not found after deploy — NestedStackProvider invariant violated.`);
|
|
31898
|
+
return this.buildOutputsAttributes(childStateData.state.outputs ?? {});
|
|
31899
|
+
}
|
|
31900
|
+
requireContext() {
|
|
31901
|
+
const ctx = getCurrentNestedStackContext();
|
|
31902
|
+
if (!ctx) throw new Error("NestedStackProvider invoked outside withNestedStackContext() scope. The deploy / destroy CLI entry point must wrap its DeployEngine.deploy / runDestroyForStack call in withNestedStackContext(ctx, () => ...).");
|
|
31903
|
+
return ctx;
|
|
31904
|
+
}
|
|
31905
|
+
requireDeployContext(ctx, op) {
|
|
31906
|
+
if (!ctx.nestedTemplates || !ctx.dagBuilder || !ctx.diffCalculator) throw new Error(`NestedStackProvider.${op}: deploy-mode context fields (nestedTemplates / dagBuilder / diffCalculator) are missing. This usually means a destroy-mode entry point called into create/update by mistake.`);
|
|
31907
|
+
}
|
|
31908
|
+
deriveChildStackName(parentStackName, nestedLogicalId) {
|
|
31909
|
+
return `${parentStackName}~${nestedLogicalId}`;
|
|
31910
|
+
}
|
|
31911
|
+
synthesizeArn(accountId, region, parentStackName, logicalId) {
|
|
31912
|
+
return `arn:cdkd-local:${region}:${accountId}:nested-stack/${parentStackName}/${logicalId}`;
|
|
31913
|
+
}
|
|
31914
|
+
extractParameters(properties) {
|
|
31915
|
+
const params = properties["Parameters"];
|
|
31916
|
+
if (!params || typeof params !== "object" || Array.isArray(params)) return {};
|
|
31917
|
+
const result = {};
|
|
31918
|
+
for (const [k, v] of Object.entries(params)) if (typeof v === "string") result[k] = v;
|
|
31919
|
+
else if (typeof v === "number" || typeof v === "boolean") result[k] = String(v);
|
|
31920
|
+
else throw new Error(`NestedStackProvider: child Parameter '${k}' resolved to a non-scalar value (type=${v === null ? "null" : typeof v}). Parameters must be scalars (string / number / boolean) by the time they reach the provider — an unresolved intrinsic here means IntrinsicFunctionResolver upstream did not handle the value, which is a bug. Surface the unresolved input rather than silently coercing to '[object Object]'.`);
|
|
31921
|
+
return result;
|
|
31922
|
+
}
|
|
31923
|
+
readChildTemplate(templatePath) {
|
|
31924
|
+
let raw;
|
|
31925
|
+
try {
|
|
31926
|
+
raw = fs.readFileSync(templatePath, "utf-8");
|
|
31927
|
+
} catch (err) {
|
|
31928
|
+
throw new Error(`Failed to read nested template at ${templatePath}: ${err instanceof Error ? err.message : String(err)}`);
|
|
31929
|
+
}
|
|
31930
|
+
try {
|
|
31931
|
+
return JSON.parse(raw);
|
|
31932
|
+
} catch (err) {
|
|
31933
|
+
throw new Error(`Failed to parse nested template at ${templatePath}: ${err instanceof Error ? err.message : String(err)}`);
|
|
31934
|
+
}
|
|
31935
|
+
}
|
|
31936
|
+
indexGrandchildTemplates(childTemplate, childTemplatePath) {
|
|
31937
|
+
const dir = path.dirname(childTemplatePath);
|
|
31938
|
+
const result = {};
|
|
31939
|
+
for (const [grandLogicalId, resource] of Object.entries(childTemplate.Resources ?? {})) {
|
|
31940
|
+
if (resource?.Type !== "AWS::CloudFormation::Stack") continue;
|
|
31941
|
+
const assetPath = resource.Metadata?.["aws:asset:path"];
|
|
31942
|
+
if (typeof assetPath !== "string" || assetPath.length === 0) continue;
|
|
31943
|
+
if (path.isAbsolute(assetPath)) throw new Error(`NestedStackProvider: nested-stack '${grandLogicalId}' has Metadata['aws:asset:path']='${assetPath}' which is absolute. CDK emits relative asset paths for nested templates; an absolute path indicates the synth output was hand-modified or generated by a non-CDK toolchain. Refusing to load.`);
|
|
31944
|
+
result[grandLogicalId] = path.join(dir, assetPath);
|
|
31945
|
+
}
|
|
31946
|
+
return result;
|
|
31947
|
+
}
|
|
31948
|
+
buildOutputsAttributes(outputs) {
|
|
31949
|
+
const attributes = {};
|
|
31950
|
+
for (const [key, value] of Object.entries(outputs)) attributes[`Outputs.${key}`] = value;
|
|
31951
|
+
return attributes;
|
|
31952
|
+
}
|
|
31953
|
+
};
|
|
31954
|
+
|
|
30947
31955
|
//#endregion
|
|
30948
31956
|
//#region src/provisioning/register-providers.ts
|
|
30949
31957
|
/**
|
|
@@ -31079,6 +32087,7 @@ function registerAllProviders(registry) {
|
|
|
31079
32087
|
registry.register("AWS::S3Tables::TableBucket", s3TablesProvider);
|
|
31080
32088
|
registry.register("AWS::S3Tables::Namespace", s3TablesProvider);
|
|
31081
32089
|
registry.register("AWS::S3Tables::Table", s3TablesProvider);
|
|
32090
|
+
registry.register("AWS::CloudFormation::Stack", new NestedStackProvider());
|
|
31082
32091
|
}
|
|
31083
32092
|
|
|
31084
32093
|
//#endregion
|
|
@@ -31356,7 +32365,7 @@ async function deployCommand(stacks, options) {
|
|
|
31356
32365
|
if (!await promptMigrationConfirm(pending, { yes: options.yes })) return;
|
|
31357
32366
|
}
|
|
31358
32367
|
}
|
|
31359
|
-
const
|
|
32368
|
+
const deployEngineOptions = {
|
|
31360
32369
|
concurrency: options.concurrency,
|
|
31361
32370
|
dryRun: options.dryRun,
|
|
31362
32371
|
noRollback: !options.rollback,
|
|
@@ -31365,7 +32374,23 @@ async function deployCommand(stacks, options) {
|
|
|
31365
32374
|
...options.resourceTimeout?.globalMs !== void 0 && { resourceTimeoutMs: options.resourceTimeout.globalMs },
|
|
31366
32375
|
...options.resourceWarnAfter?.perTypeMs && { resourceWarnAfterByType: options.resourceWarnAfter.perTypeMs },
|
|
31367
32376
|
...options.resourceTimeout?.perTypeMs && { resourceTimeoutByType: options.resourceTimeout.perTypeMs }
|
|
31368
|
-
}
|
|
32377
|
+
};
|
|
32378
|
+
const stackDeployEngine = new DeployEngine(stackStateBackend, stackLockManager, dagBuilder, diffCalculator, stackProviderRegistry, deployEngineOptions, stackRegion, exportIndexStore);
|
|
32379
|
+
const deployResult = await withNestedStackContext({
|
|
32380
|
+
stateBackend: stackStateBackend,
|
|
32381
|
+
lockManager: stackLockManager,
|
|
32382
|
+
providerRegistry: stackProviderRegistry,
|
|
32383
|
+
parentStackName: stackInfo.stackName,
|
|
32384
|
+
parentRegion: stackRegion,
|
|
32385
|
+
accountId,
|
|
32386
|
+
awsClients: stackAwsClients,
|
|
32387
|
+
stateBucket,
|
|
32388
|
+
exportIndexStore,
|
|
32389
|
+
nestedTemplates: stackInfo.nestedTemplates ?? {},
|
|
32390
|
+
dagBuilder,
|
|
32391
|
+
diffCalculator,
|
|
32392
|
+
options: deployEngineOptions
|
|
32393
|
+
}, () => stackDeployEngine.deploy(stackInfo.stackName, stackInfo.template));
|
|
31369
32394
|
logger.info(`\n${bold("Deployment Summary:")}`);
|
|
31370
32395
|
logger.info(` Stack: ${bold(cyan(deployResult.stackName))}`);
|
|
31371
32396
|
logger.info(` Created: ${deployResult.created > 0 ? green(deployResult.created) : gray(deployResult.created)}`);
|
|
@@ -32475,342 +33500,6 @@ function createDriftCommand() {
|
|
|
32475
33500
|
return cmd;
|
|
32476
33501
|
}
|
|
32477
33502
|
|
|
32478
|
-
//#endregion
|
|
32479
|
-
//#region src/cli/commands/destroy-runner.ts
|
|
32480
|
-
/**
|
|
32481
|
-
* Resource-type → state-property name pairs that gate AWS deletion
|
|
32482
|
-
* protection. Used by the `--remove-protection` confirmation prompt to
|
|
32483
|
-
* report a best-effort count of resources that will have protection
|
|
32484
|
-
* cleared. The actual flip-off is unconditional inside each provider's
|
|
32485
|
-
* `delete()` (idempotent — safe when AWS already has protection off),
|
|
32486
|
-
* so the count is informational only.
|
|
32487
|
-
*
|
|
32488
|
-
* Most types use a boolean flag — the value `true` is what we count.
|
|
32489
|
-
* Two types use a string-valued enum (Cognito UserPool's
|
|
32490
|
-
* `DeletionProtection` is `'ACTIVE' | 'INACTIVE'`, AutoScalingGroup's
|
|
32491
|
-
* `DeletionProtection` is `'none' | 'prevent-force-deletion' |
|
|
32492
|
-
* 'prevent-all-deletion'`). For those, the helper checks against a
|
|
32493
|
-
* per-type set of "active" values via `PROTECTION_ACTIVE_VALUES_BY_TYPE`.
|
|
32494
|
-
*
|
|
32495
|
-
* Exported for unit-test coverage of `countProtectedResources`.
|
|
32496
|
-
*/
|
|
32497
|
-
const PROTECTION_PROPERTY_BY_TYPE = {
|
|
32498
|
-
"AWS::Logs::LogGroup": "DeletionProtectionEnabled",
|
|
32499
|
-
"AWS::RDS::DBInstance": "DeletionProtection",
|
|
32500
|
-
"AWS::RDS::DBCluster": "DeletionProtection",
|
|
32501
|
-
"AWS::DocDB::DBCluster": "DeletionProtection",
|
|
32502
|
-
"AWS::Neptune::DBCluster": "DeletionProtection",
|
|
32503
|
-
"AWS::Neptune::DBInstance": "DeletionProtection",
|
|
32504
|
-
"AWS::DynamoDB::Table": "DeletionProtectionEnabled",
|
|
32505
|
-
"AWS::DynamoDB::GlobalTable": "DeletionProtectionEnabled",
|
|
32506
|
-
"AWS::EC2::Instance": "DisableApiTermination",
|
|
32507
|
-
"AWS::Cognito::UserPool": "DeletionProtection",
|
|
32508
|
-
"AWS::AutoScaling::AutoScalingGroup": "DeletionProtection"
|
|
32509
|
-
};
|
|
32510
|
-
/**
|
|
32511
|
-
* For string-valued protection enums, the set of values that count as
|
|
32512
|
-
* "currently protected". Types absent from this map use the default
|
|
32513
|
-
* (boolean `true`).
|
|
32514
|
-
*/
|
|
32515
|
-
const PROTECTION_ACTIVE_VALUES_BY_TYPE = {
|
|
32516
|
-
"AWS::Cognito::UserPool": new Set(["ACTIVE"]),
|
|
32517
|
-
"AWS::AutoScaling::AutoScalingGroup": new Set(["prevent-force-deletion", "prevent-all-deletion"])
|
|
32518
|
-
};
|
|
32519
|
-
/**
|
|
32520
|
-
* Count how many resources in a stack's recorded state appear to have
|
|
32521
|
-
* deletion protection enabled. Walks `properties` and `observedProperties`
|
|
32522
|
-
* for the property name registered against each resource type in
|
|
32523
|
-
* `PROTECTION_PROPERTY_BY_TYPE`. ELBv2 LoadBalancer protection lives in
|
|
32524
|
-
* `LoadBalancerAttributes` (a CFn `Array<{Key, Value}>`), so it's
|
|
32525
|
-
* handled separately via the `deletion_protection.enabled` key.
|
|
32526
|
-
*/
|
|
32527
|
-
function countProtectedResources(state) {
|
|
32528
|
-
let count = 0;
|
|
32529
|
-
for (const resource of Object.values(state.resources ?? {})) {
|
|
32530
|
-
const propName = PROTECTION_PROPERTY_BY_TYPE[resource.resourceType];
|
|
32531
|
-
if (propName) {
|
|
32532
|
-
const recorded = resource.properties?.[propName] ?? resource.observedProperties?.[propName];
|
|
32533
|
-
const activeValues = PROTECTION_ACTIVE_VALUES_BY_TYPE[resource.resourceType];
|
|
32534
|
-
if (activeValues) {
|
|
32535
|
-
if (activeValues.has(recorded)) count++;
|
|
32536
|
-
} else if (recorded === true) count++;
|
|
32537
|
-
continue;
|
|
32538
|
-
}
|
|
32539
|
-
if (resource.resourceType === "AWS::ElasticLoadBalancingV2::LoadBalancer") {
|
|
32540
|
-
if (((resource.properties?.["LoadBalancerAttributes"] ?? resource.observedProperties?.["LoadBalancerAttributes"])?.find((a) => a?.Key === "deletion_protection.enabled"))?.Value === "true") count++;
|
|
32541
|
-
}
|
|
32542
|
-
}
|
|
32543
|
-
return count;
|
|
32544
|
-
}
|
|
32545
|
-
/**
|
|
32546
|
-
* Run the destroy lifecycle for one stack against an already-loaded
|
|
32547
|
-
* `StackState`, reusing the caller's state backend / lock manager.
|
|
32548
|
-
*
|
|
32549
|
-
* Hoisted from `cdkd destroy` so the new `cdkd state destroy` subcommand
|
|
32550
|
-
* can call into the exact same per-stack pipeline without depending on
|
|
32551
|
-
* synth or the CDK app. The state-source split is the only meaningful
|
|
32552
|
-
* difference between the two commands — everything from "prompt the user"
|
|
32553
|
-
* onwards is identical.
|
|
32554
|
-
*
|
|
32555
|
-
* Side effects:
|
|
32556
|
-
* - Acquires (and releases) the stack's S3 lock.
|
|
32557
|
-
* - Switches `process.env.AWS_REGION` for the duration of the destroy when
|
|
32558
|
-
* the stack's recorded region differs from `baseRegion`. Restored in the
|
|
32559
|
-
* `finally` block.
|
|
32560
|
-
* - On full success, deletes the state file. On any failure, the state
|
|
32561
|
-
* file is preserved so the user can retry.
|
|
32562
|
-
*/
|
|
32563
|
-
async function runDestroyForStack(stackName, state, ctx) {
|
|
32564
|
-
const logger = getLogger();
|
|
32565
|
-
const result = {
|
|
32566
|
-
stackName,
|
|
32567
|
-
cancelled: false,
|
|
32568
|
-
skippedEmpty: false,
|
|
32569
|
-
deletedCount: 0,
|
|
32570
|
-
retainedCount: 0,
|
|
32571
|
-
errorCount: 0
|
|
32572
|
-
};
|
|
32573
|
-
const resourceCount = Object.keys(state.resources).length;
|
|
32574
|
-
const regionForState = state.region ?? ctx.baseRegion;
|
|
32575
|
-
if (resourceCount === 0) {
|
|
32576
|
-
logger.info(`Stack ${stackName} has no resources, cleaning up state...`);
|
|
32577
|
-
await ctx.stateBackend.deleteState(stackName, regionForState);
|
|
32578
|
-
logger.info(`${green("✓")} State deleted`);
|
|
32579
|
-
result.skippedEmpty = true;
|
|
32580
|
-
return result;
|
|
32581
|
-
}
|
|
32582
|
-
const needsStrongRefCheck = !!(state.outputs && Object.keys(state.outputs).length > 0);
|
|
32583
|
-
if (needsStrongRefCheck) {
|
|
32584
|
-
const consumers = await scanActiveConsumers(stackName, regionForState, ctx);
|
|
32585
|
-
if (consumers.length > 0) throw new StackHasActiveImportsError(stackName, regionForState, consumers);
|
|
32586
|
-
}
|
|
32587
|
-
logger.info(`\nResources to be deleted (${resourceCount}):`);
|
|
32588
|
-
for (const [logicalId, resource] of Object.entries(state.resources)) logger.info(` - ${logicalId} (${resource.resourceType})`);
|
|
32589
|
-
const protectedCount = ctx.removeProtection ? countProtectedResources(state) : 0;
|
|
32590
|
-
if (!ctx.skipConfirmation) {
|
|
32591
|
-
const rl = readline.createInterface({
|
|
32592
|
-
input: process.stdin,
|
|
32593
|
-
output: process.stdout
|
|
32594
|
-
});
|
|
32595
|
-
const prompt = ctx.removeProtection ? `\nAbout to destroy ${resourceCount} resources from stack "${stackName}", REMOVING DELETION PROTECTION on ${protectedCount} of them. Continue? (y/N): ` : `\nAre you sure you want to destroy stack "${stackName}" and delete all ${resourceCount} resources? (Y/n): `;
|
|
32596
|
-
const answer = await rl.question(prompt);
|
|
32597
|
-
rl.close();
|
|
32598
|
-
const trimmed = answer.trim().toLowerCase();
|
|
32599
|
-
if (ctx.removeProtection) {
|
|
32600
|
-
if (trimmed !== "y" && trimmed !== "yes") {
|
|
32601
|
-
logger.info("Destroy cancelled");
|
|
32602
|
-
result.cancelled = true;
|
|
32603
|
-
return result;
|
|
32604
|
-
}
|
|
32605
|
-
} else if (trimmed === "n" || trimmed === "no") {
|
|
32606
|
-
logger.info("Destroy cancelled");
|
|
32607
|
-
result.cancelled = true;
|
|
32608
|
-
return result;
|
|
32609
|
-
}
|
|
32610
|
-
}
|
|
32611
|
-
const stackRegion = state.region;
|
|
32612
|
-
let destroyProviderRegistry = ctx.providerRegistry;
|
|
32613
|
-
let destroyAwsClients;
|
|
32614
|
-
if (stackRegion && stackRegion !== ctx.baseRegion) {
|
|
32615
|
-
logger.info(`Stack region: ${stackRegion}`);
|
|
32616
|
-
process.env["AWS_REGION"] = stackRegion;
|
|
32617
|
-
process.env["AWS_DEFAULT_REGION"] = stackRegion;
|
|
32618
|
-
destroyAwsClients = new AwsClients({
|
|
32619
|
-
region: stackRegion,
|
|
32620
|
-
...ctx.profile && { profile: ctx.profile }
|
|
32621
|
-
});
|
|
32622
|
-
setAwsClients(destroyAwsClients);
|
|
32623
|
-
destroyProviderRegistry = new ProviderRegistry();
|
|
32624
|
-
registerAllProviders(destroyProviderRegistry);
|
|
32625
|
-
destroyProviderRegistry.setCustomResourceResponseBucket(ctx.stateBucket);
|
|
32626
|
-
}
|
|
32627
|
-
logger.info(`\nAcquiring lock for stack ${stackName}...`);
|
|
32628
|
-
await ctx.lockManager.acquireLock(stackName, regionForState, void 0, "destroy");
|
|
32629
|
-
if (needsStrongRefCheck) {
|
|
32630
|
-
const consumers = await scanActiveConsumers(stackName, regionForState, ctx);
|
|
32631
|
-
if (consumers.length > 0) {
|
|
32632
|
-
try {
|
|
32633
|
-
await ctx.lockManager.releaseLock(stackName, regionForState);
|
|
32634
|
-
} catch (releaseErr) {
|
|
32635
|
-
logger.warn(`Failed to release lock after strong-ref refusal: ${releaseErr instanceof Error ? releaseErr.message : String(releaseErr)}`);
|
|
32636
|
-
}
|
|
32637
|
-
throw new StackHasActiveImportsError(stackName, regionForState, consumers);
|
|
32638
|
-
}
|
|
32639
|
-
}
|
|
32640
|
-
const renderer = getLiveRenderer();
|
|
32641
|
-
renderer.start();
|
|
32642
|
-
try {
|
|
32643
|
-
logger.info("Building dependency graph...");
|
|
32644
|
-
const template = {
|
|
32645
|
-
AWSTemplateFormatVersion: "2010-09-09",
|
|
32646
|
-
Resources: {}
|
|
32647
|
-
};
|
|
32648
|
-
for (const [logicalId, resource] of Object.entries(state.resources)) template.Resources[logicalId] = {
|
|
32649
|
-
Type: resource.resourceType,
|
|
32650
|
-
Properties: resource.properties || {},
|
|
32651
|
-
...resource.dependencies && resource.dependencies.length > 0 && { DependsOn: resource.dependencies }
|
|
32652
|
-
};
|
|
32653
|
-
const typeToLogicalIds = /* @__PURE__ */ new Map();
|
|
32654
|
-
for (const [logicalId, resource] of Object.entries(state.resources)) {
|
|
32655
|
-
const ids = typeToLogicalIds.get(resource.resourceType) ?? [];
|
|
32656
|
-
ids.push(logicalId);
|
|
32657
|
-
typeToLogicalIds.set(resource.resourceType, ids);
|
|
32658
|
-
}
|
|
32659
|
-
for (const [logicalId, resource] of Object.entries(state.resources)) {
|
|
32660
|
-
const mustDeleteAfter = IMPLICIT_DELETE_DEPENDENCIES[resource.resourceType];
|
|
32661
|
-
if (!mustDeleteAfter) continue;
|
|
32662
|
-
for (const depType of mustDeleteAfter) {
|
|
32663
|
-
const depIds = typeToLogicalIds.get(depType);
|
|
32664
|
-
if (!depIds) continue;
|
|
32665
|
-
for (const depId of depIds) {
|
|
32666
|
-
const existing = template.Resources[depId]?.DependsOn ?? [];
|
|
32667
|
-
const depsArray = Array.isArray(existing) ? existing : [existing];
|
|
32668
|
-
if (!depsArray.includes(logicalId)) {
|
|
32669
|
-
template.Resources[depId] = {
|
|
32670
|
-
...template.Resources[depId],
|
|
32671
|
-
DependsOn: [...depsArray, logicalId]
|
|
32672
|
-
};
|
|
32673
|
-
logger.debug(`Implicit delete dependency: ${depId} (${depType}) must be deleted before ${logicalId} (${resource.resourceType})`);
|
|
32674
|
-
}
|
|
32675
|
-
}
|
|
32676
|
-
}
|
|
32677
|
-
}
|
|
32678
|
-
const dagBuilder = new DagBuilder();
|
|
32679
|
-
const graph = dagBuilder.buildGraph(template);
|
|
32680
|
-
const executionLevels = dagBuilder.getExecutionLevels(graph);
|
|
32681
|
-
logger.debug(`Dependency graph: ${executionLevels.length} level(s)`);
|
|
32682
|
-
for (let levelIndex = executionLevels.length - 1; levelIndex >= 0; levelIndex--) {
|
|
32683
|
-
const level = executionLevels[levelIndex];
|
|
32684
|
-
if (!level) continue;
|
|
32685
|
-
logger.debug(`Deletion level ${executionLevels.length - levelIndex}/${executionLevels.length} (${level.length} resources)`);
|
|
32686
|
-
const stackRegion = state.region ?? ctx.baseRegion;
|
|
32687
|
-
const deletePromises = level.map(async (logicalId) => {
|
|
32688
|
-
const resource = state.resources[logicalId];
|
|
32689
|
-
if (!resource) {
|
|
32690
|
-
logger.warn(`Resource ${logicalId} not found in state, skipping`);
|
|
32691
|
-
return;
|
|
32692
|
-
}
|
|
32693
|
-
if (shouldRetainResource(resource.deletionPolicy)) {
|
|
32694
|
-
logger.info(` ⊘ ${logicalId} (${resource.resourceType}) retained — DeletionPolicy: ${resource.deletionPolicy}`);
|
|
32695
|
-
result.retainedCount++;
|
|
32696
|
-
return;
|
|
32697
|
-
}
|
|
32698
|
-
const baseLabel = `Deleting ${logicalId} (${resource.resourceType})`;
|
|
32699
|
-
renderer.addTask(logicalId, baseLabel);
|
|
32700
|
-
try {
|
|
32701
|
-
const provider = destroyProviderRegistry.getProvider(resource.resourceType);
|
|
32702
|
-
const providerMinTimeoutMs = provider.getMinResourceTimeoutMs?.() ?? 0;
|
|
32703
|
-
const warnAfterMs = ctx.resourceWarnAfterByType?.[resource.resourceType] ?? ctx.resourceWarnAfterMs ?? 3e5;
|
|
32704
|
-
const globalTimeoutMs = ctx.resourceTimeoutMs ?? 18e5;
|
|
32705
|
-
const timeoutMs = ctx.resourceTimeoutByType?.[resource.resourceType] ?? Math.max(providerMinTimeoutMs, globalTimeoutMs);
|
|
32706
|
-
await withResourceDeadline(async () => {
|
|
32707
|
-
const maxAttempts = provider.disableOuterRetry ? 0 : 3;
|
|
32708
|
-
let lastDeleteError;
|
|
32709
|
-
for (let attempt = 0; attempt <= maxAttempts; attempt++) try {
|
|
32710
|
-
await provider.delete(logicalId, resource.physicalId, resource.resourceType, resource.properties, {
|
|
32711
|
-
...state.region !== void 0 && { expectedRegion: state.region },
|
|
32712
|
-
...ctx.removeProtection === true && { removeProtection: true }
|
|
32713
|
-
});
|
|
32714
|
-
lastDeleteError = null;
|
|
32715
|
-
break;
|
|
32716
|
-
} catch (retryError) {
|
|
32717
|
-
lastDeleteError = retryError;
|
|
32718
|
-
const msg = retryError instanceof Error ? retryError.message : String(retryError);
|
|
32719
|
-
if (!(msg.includes("Too Many Requests") || msg.includes("has dependencies") || msg.includes("can't be deleted since") || msg.includes("DependencyViolation")) || attempt >= maxAttempts) break;
|
|
32720
|
-
const delay = 5e3 * Math.pow(2, attempt);
|
|
32721
|
-
logger.debug(` ⏳ Retrying delete ${logicalId} in ${delay / 1e3}s (attempt ${attempt + 1}/${maxAttempts})`);
|
|
32722
|
-
await new Promise((resolve) => setTimeout(resolve, delay));
|
|
32723
|
-
}
|
|
32724
|
-
if (lastDeleteError) throw lastDeleteError;
|
|
32725
|
-
}, {
|
|
32726
|
-
warnAfterMs,
|
|
32727
|
-
timeoutMs,
|
|
32728
|
-
onWarn: (elapsedMs) => {
|
|
32729
|
-
const minutes = Math.max(1, Math.round(elapsedMs / 6e4));
|
|
32730
|
-
renderer.updateTaskLabel(logicalId, `${baseLabel} [taking longer than expected, ${minutes}m+]`);
|
|
32731
|
-
renderer.printAbove(() => {
|
|
32732
|
-
logger.warn(`${logicalId} (${resource.resourceType}) has been deleting for ${minutes}m — still waiting`);
|
|
32733
|
-
});
|
|
32734
|
-
},
|
|
32735
|
-
onTimeout: (elapsedMs) => new ResourceTimeoutError(logicalId, resource.resourceType, stackRegion, elapsedMs, "DELETE", timeoutMs)
|
|
32736
|
-
});
|
|
32737
|
-
renderer.removeTask(logicalId);
|
|
32738
|
-
logger.info(` ${red("✗")} ${bold(logicalId)} ${gray(`(${resource.resourceType})`)} ${red("deleted")}`);
|
|
32739
|
-
result.deletedCount++;
|
|
32740
|
-
} catch (error) {
|
|
32741
|
-
renderer.removeTask(logicalId);
|
|
32742
|
-
const msg = error instanceof Error ? error.message : String(error);
|
|
32743
|
-
if (msg.includes("does not exist") || msg.includes("not found") || msg.includes("No policy found") || msg.includes("NoSuchEntity") || msg.includes("NotFoundException")) {
|
|
32744
|
-
logger.debug(` ${logicalId} already deleted, removing from state`);
|
|
32745
|
-
result.deletedCount++;
|
|
32746
|
-
} else if (error instanceof ResourceTimeoutError) {
|
|
32747
|
-
const wrapped = new ProvisioningError(error.message, resource.resourceType, logicalId, resource.physicalId, error);
|
|
32748
|
-
logger.error(` ✗ Failed to delete ${logicalId}:`, wrapped.message);
|
|
32749
|
-
result.errorCount++;
|
|
32750
|
-
} else {
|
|
32751
|
-
logger.error(` ✗ Failed to delete ${logicalId}:`, String(error));
|
|
32752
|
-
result.errorCount++;
|
|
32753
|
-
}
|
|
32754
|
-
} finally {
|
|
32755
|
-
renderer.removeTask(logicalId);
|
|
32756
|
-
}
|
|
32757
|
-
});
|
|
32758
|
-
await Promise.all(deletePromises);
|
|
32759
|
-
}
|
|
32760
|
-
if (result.errorCount === 0) {
|
|
32761
|
-
await ctx.stateBackend.deleteState(stackName, regionForState);
|
|
32762
|
-
logger.debug("State deleted");
|
|
32763
|
-
if (ctx.exportIndexStore) await ctx.exportIndexStore.removeStack(stackName, regionForState);
|
|
32764
|
-
} else logger.warn(`${result.errorCount} resource(s) failed to delete. State preserved.`);
|
|
32765
|
-
const retainedSuffix = result.retainedCount > 0 ? `, ${result.retainedCount} retained` : "";
|
|
32766
|
-
if (result.errorCount === 0) logger.info(`\n${green("✓")} ${bold(`Stack ${stackName} destroyed`)} (${green(result.deletedCount)} deleted${retainedSuffix}, ${result.errorCount} errors)`);
|
|
32767
|
-
else logger.warn(`\n${yellow("⚠")} ${bold(`Stack ${stackName} partially destroyed`)} (${green(result.deletedCount)} deleted${retainedSuffix}, ${red(result.errorCount)} errors). State preserved — re-run 'cdkd destroy' / 'cdkd state destroy' to clean up.`);
|
|
32768
|
-
} finally {
|
|
32769
|
-
renderer.stop();
|
|
32770
|
-
logger.debug("Releasing lock...");
|
|
32771
|
-
await ctx.lockManager.releaseLock(stackName, regionForState);
|
|
32772
|
-
if (destroyAwsClients) {
|
|
32773
|
-
destroyAwsClients.destroy();
|
|
32774
|
-
process.env["AWS_REGION"] = ctx.baseRegion;
|
|
32775
|
-
process.env["AWS_DEFAULT_REGION"] = ctx.baseRegion;
|
|
32776
|
-
setAwsClients(ctx.baseAwsClients);
|
|
32777
|
-
}
|
|
32778
|
-
}
|
|
32779
|
-
return result;
|
|
32780
|
-
}
|
|
32781
|
-
/**
|
|
32782
|
-
* Strong-reference scan: read every other stack's state.json from the
|
|
32783
|
-
* state bucket and check whether any of its `imports[]` entries names
|
|
32784
|
-
* `producerStack`. Returns the list of offending consumers (possibly
|
|
32785
|
-
* empty).
|
|
32786
|
-
*
|
|
32787
|
-
* NEVER trusts the persistent exports index — a stale index could miss
|
|
32788
|
-
* a freshly-recorded consumer and let a destructive destroy through.
|
|
32789
|
-
* The cost is one `listStacks` + N parallel GETs at destroy time only
|
|
32790
|
-
* (not the deploy hot path), which the user-facing UX rationalizes as
|
|
32791
|
-
* the "destroy is slow OK" trade-off (Issue #343).
|
|
32792
|
-
*/
|
|
32793
|
-
async function scanActiveConsumers(producerStack, producerRegion, ctx) {
|
|
32794
|
-
const refs = await ctx.stateBackend.listStacks();
|
|
32795
|
-
return (await Promise.all(refs.map(async (ref) => {
|
|
32796
|
-
const region = ref.region ?? ctx.baseRegion;
|
|
32797
|
-
if (ref.stackName === producerStack && region === producerRegion) return null;
|
|
32798
|
-
try {
|
|
32799
|
-
const imports = (await ctx.stateBackend.getState(ref.stackName, region))?.state.imports;
|
|
32800
|
-
if (!imports || imports.length === 0) return null;
|
|
32801
|
-
const matches = imports.filter((entry) => entry.sourceStack === producerStack && entry.sourceRegion === producerRegion);
|
|
32802
|
-
if (matches.length === 0) return null;
|
|
32803
|
-
return matches.map((entry) => ({
|
|
32804
|
-
consumerStack: ref.stackName,
|
|
32805
|
-
consumerRegion: region,
|
|
32806
|
-
exportName: entry.exportName
|
|
32807
|
-
}));
|
|
32808
|
-
} catch {
|
|
32809
|
-
return null;
|
|
32810
|
-
}
|
|
32811
|
-
}))).filter((r) => r !== null).flat();
|
|
32812
|
-
}
|
|
32813
|
-
|
|
32814
33503
|
//#endregion
|
|
32815
33504
|
//#region src/cli/commands/destroy.ts
|
|
32816
33505
|
/**
|
|
@@ -32904,6 +33593,7 @@ async function destroyCommand(stackArgs, options) {
|
|
|
32904
33593
|
return;
|
|
32905
33594
|
}
|
|
32906
33595
|
logger.info(`Found ${stackNames.length} stack(s) to destroy: ${stackNames.join(", ")}`);
|
|
33596
|
+
const accountId = "unknown";
|
|
32907
33597
|
const stateRefsByName = /* @__PURE__ */ new Map();
|
|
32908
33598
|
for (const ref of allStateRefs) {
|
|
32909
33599
|
const arr = stateRefsByName.get(ref.stackName) ?? [];
|
|
@@ -32941,7 +33631,25 @@ async function destroyCommand(stackArgs, options) {
|
|
|
32941
33631
|
logger.warn(`No state found for stack ${stackName}, skipping`);
|
|
32942
33632
|
continue;
|
|
32943
33633
|
}
|
|
32944
|
-
const result = await
|
|
33634
|
+
const result = await withNestedStackContext({
|
|
33635
|
+
stateBackend,
|
|
33636
|
+
lockManager,
|
|
33637
|
+
providerRegistry,
|
|
33638
|
+
parentStackName: stackName,
|
|
33639
|
+
parentRegion: stackTargetRegion,
|
|
33640
|
+
accountId,
|
|
33641
|
+
awsClients,
|
|
33642
|
+
stateBucket,
|
|
33643
|
+
exportIndexStore,
|
|
33644
|
+
destroyOptions: {
|
|
33645
|
+
...options.profile && { profile: options.profile },
|
|
33646
|
+
...options.removeProtection === true && { removeProtection: true },
|
|
33647
|
+
...options.resourceWarnAfter?.globalMs !== void 0 && { resourceWarnAfterMs: options.resourceWarnAfter.globalMs },
|
|
33648
|
+
...options.resourceTimeout?.globalMs !== void 0 && { resourceTimeoutMs: options.resourceTimeout.globalMs },
|
|
33649
|
+
...options.resourceWarnAfter?.perTypeMs && { resourceWarnAfterByType: options.resourceWarnAfter.perTypeMs },
|
|
33650
|
+
...options.resourceTimeout?.perTypeMs && { resourceTimeoutByType: options.resourceTimeout.perTypeMs }
|
|
33651
|
+
}
|
|
33652
|
+
}, () => runDestroyForStack(stackName, stateResult.state, {
|
|
32945
33653
|
stateBackend,
|
|
32946
33654
|
lockManager,
|
|
32947
33655
|
providerRegistry,
|
|
@@ -32956,7 +33664,7 @@ async function destroyCommand(stackArgs, options) {
|
|
|
32956
33664
|
...options.resourceTimeout?.globalMs !== void 0 && { resourceTimeoutMs: options.resourceTimeout.globalMs },
|
|
32957
33665
|
...options.resourceWarnAfter?.perTypeMs && { resourceWarnAfterByType: options.resourceWarnAfter.perTypeMs },
|
|
32958
33666
|
...options.resourceTimeout?.perTypeMs && { resourceTimeoutByType: options.resourceTimeout.perTypeMs }
|
|
32959
|
-
});
|
|
33667
|
+
}));
|
|
32960
33668
|
totalErrors += result.errorCount;
|
|
32961
33669
|
}
|
|
32962
33670
|
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.`);
|
|
@@ -34690,7 +35398,25 @@ async function stateDestroyCommand(stackArgs, options) {
|
|
|
34690
35398
|
logger.warn(`No state found for stack ${stackName}${ref.region ? ` in ${ref.region}` : ""}, skipping`);
|
|
34691
35399
|
continue;
|
|
34692
35400
|
}
|
|
34693
|
-
const result = await
|
|
35401
|
+
const result = await withNestedStackContext({
|
|
35402
|
+
stateBackend: setup.stateBackend,
|
|
35403
|
+
lockManager: setup.lockManager,
|
|
35404
|
+
providerRegistry,
|
|
35405
|
+
parentStackName: stackName,
|
|
35406
|
+
parentRegion: ref.region ?? setup.region,
|
|
35407
|
+
accountId: "unknown",
|
|
35408
|
+
awsClients: setup.awsClients,
|
|
35409
|
+
stateBucket: setup.bucket,
|
|
35410
|
+
exportIndexStore: setup.exportIndexStore,
|
|
35411
|
+
destroyOptions: {
|
|
35412
|
+
...options.profile && { profile: options.profile },
|
|
35413
|
+
...options.removeProtection === true && { removeProtection: true },
|
|
35414
|
+
...options.resourceWarnAfter?.globalMs !== void 0 && { resourceWarnAfterMs: options.resourceWarnAfter.globalMs },
|
|
35415
|
+
...options.resourceTimeout?.globalMs !== void 0 && { resourceTimeoutMs: options.resourceTimeout.globalMs },
|
|
35416
|
+
...options.resourceWarnAfter?.perTypeMs && { resourceWarnAfterByType: options.resourceWarnAfter.perTypeMs },
|
|
35417
|
+
...options.resourceTimeout?.perTypeMs && { resourceTimeoutByType: options.resourceTimeout.perTypeMs }
|
|
35418
|
+
}
|
|
35419
|
+
}, () => runDestroyForStack(stackName, stateResult.state, {
|
|
34694
35420
|
stateBackend: setup.stateBackend,
|
|
34695
35421
|
lockManager: setup.lockManager,
|
|
34696
35422
|
providerRegistry,
|
|
@@ -34705,7 +35431,7 @@ async function stateDestroyCommand(stackArgs, options) {
|
|
|
34705
35431
|
...options.resourceTimeout?.globalMs !== void 0 && { resourceTimeoutMs: options.resourceTimeout.globalMs },
|
|
34706
35432
|
...options.resourceWarnAfter?.perTypeMs && { resourceWarnAfterByType: options.resourceWarnAfter.perTypeMs },
|
|
34707
35433
|
...options.resourceTimeout?.perTypeMs && { resourceTimeoutByType: options.resourceTimeout.perTypeMs }
|
|
34708
|
-
});
|
|
35434
|
+
}));
|
|
34709
35435
|
totalErrors += result.errorCount;
|
|
34710
35436
|
}
|
|
34711
35437
|
}
|
|
@@ -52812,8 +53538,11 @@ function createLocalCommand() {
|
|
|
52812
53538
|
*
|
|
52813
53539
|
* - `AWS::CDK::Metadata` is a CDK sentinel; not a real AWS resource and
|
|
52814
53540
|
* CFn refuses to import it.
|
|
52815
|
-
* - `AWS::CloudFormation::Stack` is a nested stack reference
|
|
52816
|
-
*
|
|
53541
|
+
* - `AWS::CloudFormation::Stack` is a nested stack reference. Fresh
|
|
53542
|
+
* `cdkd deploy` of nested stacks IS supported (issue #459), but
|
|
53543
|
+
* moving an existing nested-stack hierarchy from cdkd back into
|
|
53544
|
+
* CloudFormation via `cdkd export` is deferred to the
|
|
53545
|
+
* [#464](https://github.com/go-to-k/cdkd/issues/464) follow-up.
|
|
52817
53546
|
* - `AWS::CloudFormation::CustomResource` is the CFn resource type CDK
|
|
52818
53547
|
* emits for `new cdk.CustomResource(...)` when no `resourceType` is
|
|
52819
53548
|
* passed. Functionally identical to `Custom::*` — Lambda-backed,
|
|
@@ -53270,7 +53999,10 @@ async function assertCfnStackAbsent(cfnClient, stackName) {
|
|
|
53270
53999
|
* `AWS::CloudFormation::Stack` (nested stacks) is intentionally NOT in
|
|
53271
54000
|
* this set: CFn would CREATE a duplicate nested stack rather than adopt
|
|
53272
54001
|
* the existing one, which would conflict with whatever the cdkd state
|
|
53273
|
-
* thought it owned. cdkd
|
|
54002
|
+
* thought it owned. Fresh `cdkd deploy` of nested stacks is supported
|
|
54003
|
+
* via the recursive `NestedStackProvider` (#459), but `cdkd export`
|
|
54004
|
+
* adoption back into CloudFormation is deferred to the
|
|
54005
|
+
* [#464](https://github.com/go-to-k/cdkd/issues/464) follow-up.
|
|
53274
54006
|
*
|
|
53275
54007
|
* Exported for unit testing.
|
|
53276
54008
|
*/
|
|
@@ -54222,10 +54954,14 @@ function compareSemver(a, b) {
|
|
|
54222
54954
|
* different from the metadata-transfer migration this command
|
|
54223
54955
|
* provides.
|
|
54224
54956
|
*
|
|
54225
|
-
* - `AWS::CloudFormation::Stack` — nested stacks. cdkd
|
|
54226
|
-
*
|
|
54227
|
-
* flattens nested stacks into separate generated apps
|
|
54228
|
-
* that doesn't round-trip cleanly
|
|
54957
|
+
* - `AWS::CloudFormation::Stack` — nested stacks. cdkd's recursive
|
|
54958
|
+
* `NestedStackProvider` handles fresh `cdkd deploy` (#459), but
|
|
54959
|
+
* `cdk migrate` flattens nested stacks into separate generated apps
|
|
54960
|
+
* in a way that doesn't round-trip cleanly into cdkd's parent~child
|
|
54961
|
+
* state-key layout, so the migrate command keeps rejecting them.
|
|
54962
|
+
* Adopting an existing CFn-managed nested-stack hierarchy is
|
|
54963
|
+
* deferred to issue
|
|
54964
|
+
* [#464](https://github.com/go-to-k/cdkd/issues/464).
|
|
54229
54965
|
*
|
|
54230
54966
|
* - `Custom::*` — any user-defined Custom Resource type prefix.
|
|
54231
54967
|
* Same rationale as `AWS::CloudFormation::CustomResource`.
|
|
@@ -55239,7 +55975,7 @@ function reorderArgs(argv) {
|
|
|
55239
55975
|
*/
|
|
55240
55976
|
async function main() {
|
|
55241
55977
|
const program = new Command();
|
|
55242
|
-
program.name("cdkd").description("CDK Direct - Deploy AWS CDK apps directly via SDK/Cloud Control API").version("0.
|
|
55978
|
+
program.name("cdkd").description("CDK Direct - Deploy AWS CDK apps directly via SDK/Cloud Control API").version("0.145.0");
|
|
55243
55979
|
program.addCommand(createBootstrapCommand());
|
|
55244
55980
|
program.addCommand(createSynthCommand());
|
|
55245
55981
|
program.addCommand(createListCommand());
|