@go-to-k/cdkd 0.58.0 → 0.59.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js CHANGED
@@ -650,7 +650,11 @@ var contextOptions = [
650
650
  var destroyOptions = [
651
651
  new Option("-f, --force", "Do not ask for confirmation before destroying the stacks").default(
652
652
  false
653
- )
653
+ ),
654
+ new Option(
655
+ "--remove-protection",
656
+ "Bypass deletion protection on protected resources by flipping the per-resource protection flag off in-place before delete. Covers stack-level terminationProtection (CDK property) and resource-level protection on AWS::Logs::LogGroup, AWS::RDS::DBInstance, AWS::RDS::DBCluster, AWS::DynamoDB::Table, AWS::EC2::Instance, and AWS::ElasticLoadBalancingV2::LoadBalancer."
657
+ ).default(false)
654
658
  ];
655
659
 
656
660
  // src/provisioning/resource-name.ts
@@ -16684,6 +16688,7 @@ import {
16684
16688
  ListTagsOfResourceCommand,
16685
16689
  TagResourceCommand as TagResourceCommand4,
16686
16690
  UntagResourceCommand as UntagResourceCommand4,
16691
+ UpdateTableCommand,
16687
16692
  ResourceNotFoundException as ResourceNotFoundException6
16688
16693
  } from "@aws-sdk/client-dynamodb";
16689
16694
  init_aws_clients();
@@ -16849,6 +16854,32 @@ var DynamoDBTableProvider = class {
16849
16854
  */
16850
16855
  async delete(logicalId, physicalId, resourceType, _properties, context) {
16851
16856
  this.logger.debug(`Deleting DynamoDB table ${logicalId}: ${physicalId}`);
16857
+ if (context?.removeProtection === true) {
16858
+ try {
16859
+ await this.dynamoDBClient.send(
16860
+ new UpdateTableCommand({
16861
+ TableName: physicalId,
16862
+ DeletionProtectionEnabled: false
16863
+ })
16864
+ );
16865
+ this.logger.debug(
16866
+ `Disabled DeletionProtectionEnabled on DynamoDB table ${logicalId}, waiting for ACTIVE`
16867
+ );
16868
+ try {
16869
+ await this.waitForTableActiveAfterUpdate(physicalId);
16870
+ } catch (waitErr) {
16871
+ this.logger.debug(
16872
+ `Could not wait for table ${physicalId} ACTIVE after disabling protection: ${waitErr instanceof Error ? waitErr.message : String(waitErr)}`
16873
+ );
16874
+ }
16875
+ } catch (flipError) {
16876
+ if (!(flipError instanceof ResourceNotFoundException6)) {
16877
+ this.logger.debug(
16878
+ `Could not disable DeletionProtectionEnabled on ${physicalId}: ${flipError instanceof Error ? flipError.message : String(flipError)}`
16879
+ );
16880
+ }
16881
+ }
16882
+ }
16852
16883
  try {
16853
16884
  await this.dynamoDBClient.send(new DeleteTableCommand({ TableName: physicalId }));
16854
16885
  this.logger.debug(`Successfully deleted DynamoDB table ${logicalId}`);
@@ -16941,6 +16972,28 @@ var DynamoDBTableProvider = class {
16941
16972
  }
16942
16973
  throw new Error(`Table ${tableName} did not reach ACTIVE status within ${maxAttempts} seconds`);
16943
16974
  }
16975
+ /**
16976
+ * Poll DescribeTable until the table reaches ACTIVE after an UpdateTable
16977
+ * call. Distinct from `waitForTableActive` because `UpdateTable`
16978
+ * transitions the table to `UPDATING` (not `CREATING`); a status
16979
+ * mismatch should not throw — just keep polling — and the call may
16980
+ * also return immediately ACTIVE on the no-op path (already disabled).
16981
+ */
16982
+ async waitForTableActiveAfterUpdate(tableName, maxAttempts = 60) {
16983
+ for (let attempt = 1; attempt <= maxAttempts; attempt++) {
16984
+ const response = await this.dynamoDBClient.send(
16985
+ new DescribeTableCommand2({ TableName: tableName })
16986
+ );
16987
+ const status = response.Table?.TableStatus;
16988
+ if (status === "ACTIVE") {
16989
+ return;
16990
+ }
16991
+ await new Promise((resolve4) => setTimeout(resolve4, 1e3));
16992
+ }
16993
+ throw new Error(
16994
+ `Table ${tableName} did not reach ACTIVE status within ${maxAttempts} seconds after UpdateTable`
16995
+ );
16996
+ }
16944
16997
  /**
16945
16998
  * Resolve a single `Fn::GetAtt` attribute for an existing DynamoDB table.
16946
16999
  *
@@ -17420,6 +17473,23 @@ var LogsLogGroupProvider = class {
17420
17473
  */
17421
17474
  async delete(logicalId, physicalId, resourceType, _properties, context) {
17422
17475
  this.logger.debug(`Deleting log group ${logicalId}: ${physicalId}`);
17476
+ if (context?.removeProtection === true) {
17477
+ try {
17478
+ await this.logsClient.send(
17479
+ new PutLogGroupDeletionProtectionCommand({
17480
+ logGroupIdentifier: physicalId,
17481
+ deletionProtectionEnabled: false
17482
+ })
17483
+ );
17484
+ this.logger.debug(
17485
+ `Disabled DeletionProtectionEnabled on log group ${logicalId} before delete`
17486
+ );
17487
+ } catch (flipError) {
17488
+ this.logger.debug(
17489
+ `Could not disable DeletionProtectionEnabled on ${physicalId}: ${flipError instanceof Error ? flipError.message : String(flipError)}`
17490
+ );
17491
+ }
17492
+ }
17423
17493
  try {
17424
17494
  await this.logsClient.send(new DeleteLogGroupCommand({ logGroupName: physicalId }));
17425
17495
  this.logger.debug(`Successfully deleted log group ${logicalId}`);
@@ -19675,7 +19745,8 @@ import {
19675
19745
  DescribeNetworkInterfacesCommand as DescribeNetworkInterfacesCommand2,
19676
19746
  DeleteNetworkInterfaceCommand as DeleteNetworkInterfaceCommand2,
19677
19747
  DescribeVolumesCommand,
19678
- DescribeInstanceAttributeCommand
19748
+ DescribeInstanceAttributeCommand,
19749
+ ModifyInstanceAttributeCommand
19679
19750
  } from "@aws-sdk/client-ec2";
19680
19751
  init_aws_clients();
19681
19752
  var EC2Provider = class {
@@ -21323,6 +21394,25 @@ var EC2Provider = class {
21323
21394
  }
21324
21395
  async deleteInstance(logicalId, physicalId, resourceType, context) {
21325
21396
  this.logger.debug(`Terminating EC2 Instance ${logicalId}: ${physicalId}`);
21397
+ if (context?.removeProtection === true) {
21398
+ try {
21399
+ await this.ec2Client.send(
21400
+ new ModifyInstanceAttributeCommand({
21401
+ InstanceId: physicalId,
21402
+ DisableApiTermination: { Value: false }
21403
+ })
21404
+ );
21405
+ this.logger.debug(
21406
+ `Disabled DisableApiTermination on EC2 Instance ${logicalId} before termination`
21407
+ );
21408
+ } catch (flipError) {
21409
+ if (!this.isNotFoundError(flipError)) {
21410
+ this.logger.debug(
21411
+ `Could not disable DisableApiTermination on ${physicalId}: ${flipError instanceof Error ? flipError.message : String(flipError)}`
21412
+ );
21413
+ }
21414
+ }
21415
+ }
21326
21416
  try {
21327
21417
  await this.ec2Client.send(new TerminateInstancesCommand({ InstanceIds: [physicalId] }));
21328
21418
  this.logger.debug(`Terminate requested for EC2 Instance ${logicalId}, waiting...`);
@@ -28246,6 +28336,25 @@ var ELBv2Provider = class {
28246
28336
  }
28247
28337
  async deleteLoadBalancer(logicalId, physicalId, resourceType, context) {
28248
28338
  this.logger.debug(`Deleting LoadBalancer ${logicalId}: ${physicalId}`);
28339
+ if (context?.removeProtection === true) {
28340
+ try {
28341
+ await this.getClient().send(
28342
+ new ModifyLoadBalancerAttributesCommand({
28343
+ LoadBalancerArn: physicalId,
28344
+ Attributes: [{ Key: "deletion_protection.enabled", Value: "false" }]
28345
+ })
28346
+ );
28347
+ this.logger.debug(
28348
+ `Disabled deletion_protection.enabled on LoadBalancer ${logicalId} before delete`
28349
+ );
28350
+ } catch (flipError) {
28351
+ if (!this.isNotFoundError(flipError)) {
28352
+ this.logger.debug(
28353
+ `Could not disable deletion_protection.enabled on ${physicalId}: ${flipError instanceof Error ? flipError.message : String(flipError)}`
28354
+ );
28355
+ }
28356
+ }
28357
+ }
28249
28358
  try {
28250
28359
  await this.getClient().send(new DeleteLoadBalancerCommand({ LoadBalancerArn: physicalId }));
28251
28360
  this.logger.debug(`Successfully deleted LoadBalancer ${logicalId}`);
@@ -29257,18 +29366,22 @@ var RDSProvider = class {
29257
29366
  async deleteDBCluster(logicalId, physicalId, resourceType, context) {
29258
29367
  this.logger.debug(`Deleting DBCluster ${logicalId}: ${physicalId}`);
29259
29368
  try {
29260
- try {
29261
- await this.getClient().send(
29262
- new ModifyDBClusterCommand({
29263
- DBClusterIdentifier: physicalId,
29264
- DeletionProtection: false
29265
- })
29266
- );
29267
- } catch (disableError) {
29268
- if (!this.isNotFoundError(disableError, "DBClusterNotFoundFault")) {
29269
- this.logger.debug(
29270
- `Could not disable deletion protection for ${physicalId}: ${disableError instanceof Error ? disableError.message : String(disableError)}`
29369
+ if (context?.removeProtection === true) {
29370
+ try {
29371
+ await this.getClient().send(
29372
+ new ModifyDBClusterCommand({
29373
+ DBClusterIdentifier: physicalId,
29374
+ DeletionProtection: false,
29375
+ ApplyImmediately: true
29376
+ })
29271
29377
  );
29378
+ this.logger.debug(`Disabled DeletionProtection on DBCluster ${logicalId} before delete`);
29379
+ } catch (disableError) {
29380
+ if (!this.isNotFoundError(disableError, "DBClusterNotFoundFault")) {
29381
+ this.logger.debug(
29382
+ `Could not disable deletion protection for ${physicalId}: ${disableError instanceof Error ? disableError.message : String(disableError)}`
29383
+ );
29384
+ }
29272
29385
  }
29273
29386
  }
29274
29387
  await this.getClient().send(
@@ -29392,19 +29505,22 @@ var RDSProvider = class {
29392
29505
  async deleteDBInstance(logicalId, physicalId, resourceType, context) {
29393
29506
  this.logger.debug(`Deleting DBInstance ${logicalId}: ${physicalId}`);
29394
29507
  try {
29395
- try {
29396
- await this.getClient().send(
29397
- new ModifyDBInstanceCommand({
29398
- DBInstanceIdentifier: physicalId,
29399
- DeletionProtection: false,
29400
- ApplyImmediately: true
29401
- })
29402
- );
29403
- } catch (disableError) {
29404
- if (!this.isNotFoundError(disableError, "DBInstanceNotFoundFault")) {
29405
- this.logger.debug(
29406
- `Could not disable deletion protection for ${physicalId}: ${disableError instanceof Error ? disableError.message : String(disableError)}`
29508
+ if (context?.removeProtection === true) {
29509
+ try {
29510
+ await this.getClient().send(
29511
+ new ModifyDBInstanceCommand({
29512
+ DBInstanceIdentifier: physicalId,
29513
+ DeletionProtection: false,
29514
+ ApplyImmediately: true
29515
+ })
29407
29516
  );
29517
+ this.logger.debug(`Disabled DeletionProtection on DBInstance ${logicalId} before delete`);
29518
+ } catch (disableError) {
29519
+ if (!this.isNotFoundError(disableError, "DBInstanceNotFoundFault")) {
29520
+ this.logger.debug(
29521
+ `Could not disable deletion protection for ${physicalId}: ${disableError instanceof Error ? disableError.message : String(disableError)}`
29522
+ );
29523
+ }
29408
29524
  }
29409
29525
  }
29410
29526
  await this.getClient().send(
@@ -31447,56 +31563,69 @@ var CognitoUserPoolProvider = class {
31447
31563
  }
31448
31564
  }
31449
31565
  /**
31450
- * Delete a Cognito User Pool
31566
+ * Delete a Cognito User Pool.
31567
+ *
31568
+ * When `context.removeProtection === true`, `DeletionProtection` is flipped
31569
+ * from `ACTIVE` to `INACTIVE` via `UpdateUserPool` before deletion. The
31570
+ * call is idempotent — AWS accepts the no-op already-disabled case
31571
+ * without error. Without `removeProtection`, AWS rejects the delete on a
31572
+ * protected pool with `InvalidParameterException` and the destroy fails;
31573
+ * the user is expected to set `--remove-protection` explicitly.
31451
31574
  *
31452
- * If DeletionProtection is ACTIVE, it is automatically disabled before deletion.
31575
+ * Pre-PR behavior was an unconditional flip-off; that silent bypass has
31576
+ * been gated on `--remove-protection` to match the rest of the
31577
+ * deletion-protection-bearing types and CDK CLI's refuse-on-protected
31578
+ * semantics. See PR body for migration notes.
31453
31579
  */
31454
31580
  async delete(logicalId, physicalId, resourceType, properties, context) {
31455
31581
  this.logger.debug(`Deleting Cognito User Pool ${logicalId}: ${physicalId}`);
31456
31582
  try {
31457
- const deletionProtection = properties?.["DeletionProtection"];
31458
- if (deletionProtection === "ACTIVE") {
31459
- this.logger.debug(
31460
- `Disabling DeletionProtection on Cognito User Pool ${physicalId} before deletion`
31461
- );
31462
- await this.getClient().send(
31463
- new UpdateUserPoolCommand({
31464
- UserPoolId: physicalId,
31465
- DeletionProtection: "INACTIVE"
31466
- })
31467
- );
31468
- } else {
31469
- try {
31470
- const describeResponse = await this.getClient().send(
31471
- new DescribeUserPoolCommand({ UserPoolId: physicalId })
31472
- );
31473
- if (describeResponse.UserPool?.DeletionProtection === "ACTIVE") {
31583
+ if (context?.removeProtection === true) {
31584
+ const templatedActive = properties?.["DeletionProtection"] === "ACTIVE";
31585
+ let needsFlip = templatedActive;
31586
+ if (!templatedActive) {
31587
+ try {
31588
+ const describeResponse = await this.getClient().send(
31589
+ new DescribeUserPoolCommand({ UserPoolId: physicalId })
31590
+ );
31591
+ needsFlip = describeResponse.UserPool?.DeletionProtection === "ACTIVE";
31592
+ } catch (descError) {
31593
+ if (descError instanceof ResourceNotFoundException12) {
31594
+ const clientRegion = await this.getClient().config.region();
31595
+ assertRegionMatch(
31596
+ clientRegion,
31597
+ context?.expectedRegion,
31598
+ resourceType,
31599
+ logicalId,
31600
+ physicalId
31601
+ );
31602
+ this.logger.debug(
31603
+ `Cognito User Pool ${physicalId} does not exist, skipping deletion`
31604
+ );
31605
+ return;
31606
+ }
31474
31607
  this.logger.debug(
31475
- `Disabling DeletionProtection on Cognito User Pool ${physicalId} before deletion`
31608
+ `Failed to describe Cognito User Pool ${physicalId}, attempting flip-off anyway`
31476
31609
  );
31610
+ needsFlip = true;
31611
+ }
31612
+ }
31613
+ if (needsFlip) {
31614
+ this.logger.debug(
31615
+ `Disabling DeletionProtection on Cognito User Pool ${physicalId} before deletion (--remove-protection)`
31616
+ );
31617
+ try {
31477
31618
  await this.getClient().send(
31478
31619
  new UpdateUserPoolCommand({
31479
31620
  UserPoolId: physicalId,
31480
31621
  DeletionProtection: "INACTIVE"
31481
31622
  })
31482
31623
  );
31483
- }
31484
- } catch (descError) {
31485
- if (descError instanceof ResourceNotFoundException12) {
31486
- const clientRegion = await this.getClient().config.region();
31487
- assertRegionMatch(
31488
- clientRegion,
31489
- context?.expectedRegion,
31490
- resourceType,
31491
- logicalId,
31492
- physicalId
31624
+ } catch (flipError) {
31625
+ this.logger.debug(
31626
+ `Could not disable DeletionProtection for ${physicalId}: ${flipError instanceof Error ? flipError.message : String(flipError)}`
31493
31627
  );
31494
- this.logger.debug(`Cognito User Pool ${physicalId} does not exist, skipping deletion`);
31495
- return;
31496
31628
  }
31497
- this.logger.debug(
31498
- `Failed to describe Cognito User Pool ${physicalId}, proceeding with delete`
31499
- );
31500
31629
  }
31501
31630
  }
31502
31631
  await this.getClient().send(new DeleteUserPoolCommand({ UserPoolId: physicalId }));
@@ -33885,7 +34014,7 @@ import {
33885
34014
  UpdateDatabaseCommand,
33886
34015
  DeleteDatabaseCommand,
33887
34016
  CreateTableCommand as CreateTableCommand2,
33888
- UpdateTableCommand,
34017
+ UpdateTableCommand as UpdateTableCommand2,
33889
34018
  DeleteTableCommand as DeleteTableCommand2,
33890
34019
  GetDatabaseCommand,
33891
34020
  GetDatabasesCommand,
@@ -34145,7 +34274,7 @@ var GlueProvider = class {
34145
34274
  const catalogId = properties["CatalogId"];
34146
34275
  try {
34147
34276
  await this.getClient().send(
34148
- new UpdateTableCommand({
34277
+ new UpdateTableCommand2({
34149
34278
  CatalogId: catalogId,
34150
34279
  DatabaseName: databaseName,
34151
34280
  TableInput: this.buildTableInput(tableInput)
@@ -39884,6 +40013,645 @@ var ECRProvider = class {
39884
40013
  }
39885
40014
  };
39886
40015
 
40016
+ // src/provisioning/providers/asg-provider.ts
40017
+ import {
40018
+ AutoScalingClient,
40019
+ CreateAutoScalingGroupCommand,
40020
+ UpdateAutoScalingGroupCommand,
40021
+ DeleteAutoScalingGroupCommand,
40022
+ DescribeAutoScalingGroupsCommand
40023
+ } from "@aws-sdk/client-auto-scaling";
40024
+ var ASGProvider = class {
40025
+ asgClient;
40026
+ providerRegion = process.env["AWS_REGION"];
40027
+ logger = getLogger().child("ASGProvider");
40028
+ handledProperties = /* @__PURE__ */ new Map([
40029
+ [
40030
+ "AWS::AutoScaling::AutoScalingGroup",
40031
+ /* @__PURE__ */ new Set([
40032
+ "AutoScalingGroupName",
40033
+ "LaunchTemplate",
40034
+ "MinSize",
40035
+ "MaxSize",
40036
+ "DesiredCapacity",
40037
+ "VPCZoneIdentifier",
40038
+ "AvailabilityZones",
40039
+ "HealthCheckType",
40040
+ "HealthCheckGracePeriod",
40041
+ "Cooldown",
40042
+ "DefaultCooldown",
40043
+ "Tags",
40044
+ "TerminationPolicies",
40045
+ "NewInstancesProtectedFromScaleIn",
40046
+ "CapacityRebalance",
40047
+ "ServiceLinkedRoleARN",
40048
+ "MaxInstanceLifetime",
40049
+ "LoadBalancerNames",
40050
+ "TargetGroupARNs",
40051
+ "MetricsCollection",
40052
+ "LifecycleHookSpecificationList",
40053
+ "MixedInstancesPolicy",
40054
+ "Context",
40055
+ "DesiredCapacityType",
40056
+ "DefaultInstanceWarmup",
40057
+ "TrafficSources",
40058
+ "AvailabilityZoneDistribution",
40059
+ "AvailabilityZoneImpairmentPolicy",
40060
+ "SkipZonalShiftValidation",
40061
+ "CapacityReservationSpecification",
40062
+ "InstanceMaintenancePolicy",
40063
+ "DeletionProtection"
40064
+ ])
40065
+ ]
40066
+ ]);
40067
+ getClient() {
40068
+ if (!this.asgClient) {
40069
+ this.asgClient = new AutoScalingClient(
40070
+ this.providerRegion ? { region: this.providerRegion } : {}
40071
+ );
40072
+ }
40073
+ return this.asgClient;
40074
+ }
40075
+ // ─── Dispatch ─────────────────────────────────────────────────────
40076
+ async create(logicalId, resourceType, properties) {
40077
+ if (resourceType !== "AWS::AutoScaling::AutoScalingGroup") {
40078
+ throw new ProvisioningError(
40079
+ `Unsupported resource type: ${resourceType}`,
40080
+ resourceType,
40081
+ logicalId
40082
+ );
40083
+ }
40084
+ const groupName = properties["AutoScalingGroupName"] || generateResourceName(logicalId, { maxLength: 255 });
40085
+ this.logger.debug(`Creating AutoScalingGroup ${logicalId}: ${groupName}`);
40086
+ try {
40087
+ const launchTemplate = this.buildLaunchTemplate(properties);
40088
+ const tags = this.buildTags(groupName, properties);
40089
+ const vpcZoneIdentifier = this.joinVpcZoneIdentifier(properties["VPCZoneIdentifier"]);
40090
+ const minSize = properties["MinSize"] != null ? Number(properties["MinSize"]) : 0;
40091
+ const maxSize = properties["MaxSize"] != null ? Number(properties["MaxSize"]) : minSize;
40092
+ await this.getClient().send(
40093
+ new CreateAutoScalingGroupCommand({
40094
+ AutoScalingGroupName: groupName,
40095
+ MinSize: minSize,
40096
+ MaxSize: maxSize,
40097
+ ...properties["DesiredCapacity"] != null && {
40098
+ DesiredCapacity: Number(properties["DesiredCapacity"])
40099
+ },
40100
+ ...launchTemplate && { LaunchTemplate: launchTemplate },
40101
+ ...properties["MixedInstancesPolicy"] !== void 0 && {
40102
+ MixedInstancesPolicy: properties["MixedInstancesPolicy"]
40103
+ },
40104
+ ...vpcZoneIdentifier !== void 0 && { VPCZoneIdentifier: vpcZoneIdentifier },
40105
+ ...properties["AvailabilityZones"] !== void 0 && {
40106
+ AvailabilityZones: properties["AvailabilityZones"]
40107
+ },
40108
+ ...properties["HealthCheckType"] !== void 0 && {
40109
+ HealthCheckType: properties["HealthCheckType"]
40110
+ },
40111
+ ...properties["HealthCheckGracePeriod"] != null && {
40112
+ HealthCheckGracePeriod: Number(properties["HealthCheckGracePeriod"])
40113
+ },
40114
+ ...properties["Cooldown"] != null && {
40115
+ DefaultCooldown: Number(properties["Cooldown"])
40116
+ },
40117
+ ...properties["DefaultCooldown"] != null && {
40118
+ DefaultCooldown: Number(properties["DefaultCooldown"])
40119
+ },
40120
+ ...properties["TerminationPolicies"] !== void 0 && {
40121
+ TerminationPolicies: properties["TerminationPolicies"]
40122
+ },
40123
+ ...properties["NewInstancesProtectedFromScaleIn"] !== void 0 && {
40124
+ NewInstancesProtectedFromScaleIn: properties["NewInstancesProtectedFromScaleIn"]
40125
+ },
40126
+ ...properties["CapacityRebalance"] !== void 0 && {
40127
+ CapacityRebalance: properties["CapacityRebalance"]
40128
+ },
40129
+ ...properties["ServiceLinkedRoleARN"] !== void 0 && {
40130
+ ServiceLinkedRoleARN: properties["ServiceLinkedRoleARN"]
40131
+ },
40132
+ ...properties["MaxInstanceLifetime"] != null && {
40133
+ MaxInstanceLifetime: Number(properties["MaxInstanceLifetime"])
40134
+ },
40135
+ ...properties["LoadBalancerNames"] !== void 0 && {
40136
+ LoadBalancerNames: properties["LoadBalancerNames"]
40137
+ },
40138
+ ...properties["TargetGroupARNs"] !== void 0 && {
40139
+ TargetGroupARNs: properties["TargetGroupARNs"]
40140
+ },
40141
+ ...properties["Context"] !== void 0 && {
40142
+ Context: properties["Context"]
40143
+ },
40144
+ ...properties["DesiredCapacityType"] !== void 0 && {
40145
+ DesiredCapacityType: properties["DesiredCapacityType"]
40146
+ },
40147
+ ...properties["DefaultInstanceWarmup"] != null && {
40148
+ DefaultInstanceWarmup: Number(properties["DefaultInstanceWarmup"])
40149
+ },
40150
+ ...properties["LifecycleHookSpecificationList"] !== void 0 && {
40151
+ LifecycleHookSpecificationList: properties["LifecycleHookSpecificationList"]
40152
+ },
40153
+ ...properties["TrafficSources"] !== void 0 && {
40154
+ TrafficSources: properties["TrafficSources"]
40155
+ },
40156
+ ...properties["AvailabilityZoneDistribution"] !== void 0 && {
40157
+ AvailabilityZoneDistribution: properties["AvailabilityZoneDistribution"]
40158
+ },
40159
+ ...properties["AvailabilityZoneImpairmentPolicy"] !== void 0 && {
40160
+ AvailabilityZoneImpairmentPolicy: properties["AvailabilityZoneImpairmentPolicy"]
40161
+ },
40162
+ ...properties["SkipZonalShiftValidation"] !== void 0 && {
40163
+ SkipZonalShiftValidation: properties["SkipZonalShiftValidation"]
40164
+ },
40165
+ ...properties["CapacityReservationSpecification"] !== void 0 && {
40166
+ CapacityReservationSpecification: properties["CapacityReservationSpecification"]
40167
+ },
40168
+ ...properties["InstanceMaintenancePolicy"] !== void 0 && {
40169
+ InstanceMaintenancePolicy: properties["InstanceMaintenancePolicy"]
40170
+ },
40171
+ ...properties["DeletionProtection"] !== void 0 && {
40172
+ DeletionProtection: properties["DeletionProtection"]
40173
+ },
40174
+ ...tags.length > 0 && { Tags: tags }
40175
+ })
40176
+ );
40177
+ this.logger.debug(`Successfully created AutoScalingGroup ${logicalId}: ${groupName}`);
40178
+ const arn = await this.fetchArn(groupName);
40179
+ const attributes = {};
40180
+ if (arn)
40181
+ attributes["Arn"] = arn;
40182
+ if (launchTemplate?.LaunchTemplateId) {
40183
+ attributes["LaunchTemplateID"] = launchTemplate.LaunchTemplateId;
40184
+ }
40185
+ return { physicalId: groupName, attributes };
40186
+ } catch (error) {
40187
+ const cause = error instanceof Error ? error : void 0;
40188
+ throw new ProvisioningError(
40189
+ `Failed to create AutoScalingGroup ${logicalId}: ${error instanceof Error ? error.message : String(error)}`,
40190
+ resourceType,
40191
+ logicalId,
40192
+ groupName,
40193
+ cause
40194
+ );
40195
+ }
40196
+ }
40197
+ async update(logicalId, physicalId, resourceType, properties, previousProperties) {
40198
+ if (resourceType !== "AWS::AutoScaling::AutoScalingGroup") {
40199
+ throw new ProvisioningError(
40200
+ `Unsupported resource type: ${resourceType}`,
40201
+ resourceType,
40202
+ logicalId,
40203
+ physicalId
40204
+ );
40205
+ }
40206
+ this.logger.debug(`Updating AutoScalingGroup ${logicalId}: ${physicalId}`);
40207
+ const stringEq = (a, b) => JSON.stringify(a) === JSON.stringify(b);
40208
+ if (!stringEq(properties["AutoScalingGroupName"], previousProperties["AutoScalingGroupName"])) {
40209
+ throw new ResourceUpdateNotSupportedError(
40210
+ resourceType,
40211
+ logicalId,
40212
+ "AutoScalingGroupName is immutable; use cdkd deploy --replace to replace the group"
40213
+ );
40214
+ }
40215
+ if (!stringEq(properties["Tags"] ?? [], previousProperties["Tags"] ?? [])) {
40216
+ throw new ResourceUpdateNotSupportedError(
40217
+ resourceType,
40218
+ logicalId,
40219
+ "Tags updates on AWS::AutoScaling::AutoScalingGroup are not yet supported by cdkd; use cdkd deploy --replace, or update the tags via AWS console / CLI"
40220
+ );
40221
+ }
40222
+ if (!stringEq(
40223
+ properties["LoadBalancerNames"] ?? [],
40224
+ previousProperties["LoadBalancerNames"] ?? []
40225
+ )) {
40226
+ throw new ResourceUpdateNotSupportedError(
40227
+ resourceType,
40228
+ logicalId,
40229
+ "LoadBalancerNames diffs require Attach/Detach calls; use cdkd deploy --replace"
40230
+ );
40231
+ }
40232
+ if (!stringEq(properties["TargetGroupARNs"] ?? [], previousProperties["TargetGroupARNs"] ?? [])) {
40233
+ throw new ResourceUpdateNotSupportedError(
40234
+ resourceType,
40235
+ logicalId,
40236
+ "TargetGroupARNs diffs require Attach/Detach calls; use cdkd deploy --replace"
40237
+ );
40238
+ }
40239
+ if (!stringEq(
40240
+ properties["LifecycleHookSpecificationList"] ?? [],
40241
+ previousProperties["LifecycleHookSpecificationList"] ?? []
40242
+ )) {
40243
+ throw new ResourceUpdateNotSupportedError(
40244
+ resourceType,
40245
+ logicalId,
40246
+ "LifecycleHookSpecificationList diffs require PutLifecycleHook / DeleteLifecycleHook calls; use cdkd deploy --replace"
40247
+ );
40248
+ }
40249
+ if (!stringEq(
40250
+ properties["MetricsCollection"] ?? [],
40251
+ previousProperties["MetricsCollection"] ?? []
40252
+ )) {
40253
+ throw new ResourceUpdateNotSupportedError(
40254
+ resourceType,
40255
+ logicalId,
40256
+ "MetricsCollection diffs require EnableMetricsCollection / DisableMetricsCollection calls; use cdkd deploy --replace"
40257
+ );
40258
+ }
40259
+ if (!stringEq(properties["TrafficSources"] ?? [], previousProperties["TrafficSources"] ?? [])) {
40260
+ throw new ResourceUpdateNotSupportedError(
40261
+ resourceType,
40262
+ logicalId,
40263
+ "TrafficSources diffs require AttachTrafficSources / DetachTrafficSources calls; use cdkd deploy --replace"
40264
+ );
40265
+ }
40266
+ try {
40267
+ const launchTemplate = this.buildLaunchTemplate(properties);
40268
+ const vpcZoneIdentifier = this.joinVpcZoneIdentifier(properties["VPCZoneIdentifier"]);
40269
+ await this.getClient().send(
40270
+ new UpdateAutoScalingGroupCommand({
40271
+ AutoScalingGroupName: physicalId,
40272
+ ...properties["MinSize"] != null && { MinSize: Number(properties["MinSize"]) },
40273
+ ...properties["MaxSize"] != null && { MaxSize: Number(properties["MaxSize"]) },
40274
+ ...properties["DesiredCapacity"] != null && {
40275
+ DesiredCapacity: Number(properties["DesiredCapacity"])
40276
+ },
40277
+ ...launchTemplate && { LaunchTemplate: launchTemplate },
40278
+ ...properties["MixedInstancesPolicy"] !== void 0 && {
40279
+ MixedInstancesPolicy: properties["MixedInstancesPolicy"]
40280
+ },
40281
+ ...vpcZoneIdentifier !== void 0 && { VPCZoneIdentifier: vpcZoneIdentifier },
40282
+ ...properties["AvailabilityZones"] !== void 0 && {
40283
+ AvailabilityZones: properties["AvailabilityZones"]
40284
+ },
40285
+ ...properties["HealthCheckType"] !== void 0 && {
40286
+ HealthCheckType: properties["HealthCheckType"]
40287
+ },
40288
+ ...properties["HealthCheckGracePeriod"] != null && {
40289
+ HealthCheckGracePeriod: Number(properties["HealthCheckGracePeriod"])
40290
+ },
40291
+ ...properties["Cooldown"] != null && {
40292
+ DefaultCooldown: Number(properties["Cooldown"])
40293
+ },
40294
+ ...properties["DefaultCooldown"] != null && {
40295
+ DefaultCooldown: Number(properties["DefaultCooldown"])
40296
+ },
40297
+ ...properties["TerminationPolicies"] !== void 0 && {
40298
+ TerminationPolicies: properties["TerminationPolicies"]
40299
+ },
40300
+ ...properties["NewInstancesProtectedFromScaleIn"] !== void 0 && {
40301
+ NewInstancesProtectedFromScaleIn: properties["NewInstancesProtectedFromScaleIn"]
40302
+ },
40303
+ ...properties["CapacityRebalance"] !== void 0 && {
40304
+ CapacityRebalance: properties["CapacityRebalance"]
40305
+ },
40306
+ ...properties["ServiceLinkedRoleARN"] !== void 0 && {
40307
+ ServiceLinkedRoleARN: properties["ServiceLinkedRoleARN"]
40308
+ },
40309
+ ...properties["MaxInstanceLifetime"] != null && {
40310
+ MaxInstanceLifetime: Number(properties["MaxInstanceLifetime"])
40311
+ },
40312
+ ...properties["Context"] !== void 0 && {
40313
+ Context: properties["Context"]
40314
+ },
40315
+ ...properties["DesiredCapacityType"] !== void 0 && {
40316
+ DesiredCapacityType: properties["DesiredCapacityType"]
40317
+ },
40318
+ ...properties["DefaultInstanceWarmup"] != null && {
40319
+ DefaultInstanceWarmup: Number(properties["DefaultInstanceWarmup"])
40320
+ },
40321
+ ...properties["AvailabilityZoneDistribution"] !== void 0 && {
40322
+ AvailabilityZoneDistribution: properties["AvailabilityZoneDistribution"]
40323
+ },
40324
+ ...properties["AvailabilityZoneImpairmentPolicy"] !== void 0 && {
40325
+ AvailabilityZoneImpairmentPolicy: properties["AvailabilityZoneImpairmentPolicy"]
40326
+ },
40327
+ ...properties["SkipZonalShiftValidation"] !== void 0 && {
40328
+ SkipZonalShiftValidation: properties["SkipZonalShiftValidation"]
40329
+ },
40330
+ ...properties["CapacityReservationSpecification"] !== void 0 && {
40331
+ CapacityReservationSpecification: properties["CapacityReservationSpecification"]
40332
+ },
40333
+ ...properties["InstanceMaintenancePolicy"] !== void 0 && {
40334
+ InstanceMaintenancePolicy: properties["InstanceMaintenancePolicy"]
40335
+ },
40336
+ ...properties["DeletionProtection"] !== void 0 && {
40337
+ DeletionProtection: properties["DeletionProtection"]
40338
+ }
40339
+ })
40340
+ );
40341
+ this.logger.debug(`Successfully updated AutoScalingGroup ${logicalId}`);
40342
+ const arn = await this.fetchArn(physicalId);
40343
+ const attributes = {};
40344
+ if (arn)
40345
+ attributes["Arn"] = arn;
40346
+ if (launchTemplate?.LaunchTemplateId) {
40347
+ attributes["LaunchTemplateID"] = launchTemplate.LaunchTemplateId;
40348
+ }
40349
+ return { physicalId, wasReplaced: false, attributes };
40350
+ } catch (error) {
40351
+ if (error instanceof ResourceUpdateNotSupportedError)
40352
+ throw error;
40353
+ const cause = error instanceof Error ? error : void 0;
40354
+ throw new ProvisioningError(
40355
+ `Failed to update AutoScalingGroup ${logicalId}: ${error instanceof Error ? error.message : String(error)}`,
40356
+ resourceType,
40357
+ logicalId,
40358
+ physicalId,
40359
+ cause
40360
+ );
40361
+ }
40362
+ }
40363
+ async delete(logicalId, physicalId, resourceType, _properties, context) {
40364
+ this.logger.debug(`Deleting AutoScalingGroup ${logicalId}: ${physicalId}`);
40365
+ if (context?.removeProtection === true) {
40366
+ try {
40367
+ await this.getClient().send(
40368
+ new UpdateAutoScalingGroupCommand({
40369
+ AutoScalingGroupName: physicalId,
40370
+ DeletionProtection: "none"
40371
+ })
40372
+ );
40373
+ this.logger.debug(
40374
+ `Disabled DeletionProtection on AutoScalingGroup ${logicalId} before delete`
40375
+ );
40376
+ } catch (flipError) {
40377
+ this.logger.debug(
40378
+ `Could not disable DeletionProtection on ${physicalId}: ${flipError instanceof Error ? flipError.message : String(flipError)}`
40379
+ );
40380
+ }
40381
+ }
40382
+ try {
40383
+ await this.getClient().send(
40384
+ new DeleteAutoScalingGroupCommand({
40385
+ AutoScalingGroupName: physicalId,
40386
+ ForceDelete: context?.removeProtection === true
40387
+ })
40388
+ );
40389
+ this.logger.debug(`Successfully initiated deletion of AutoScalingGroup ${logicalId}`);
40390
+ await this.waitForGroupDeleted(physicalId);
40391
+ } catch (error) {
40392
+ if (this.isNotFoundError(error)) {
40393
+ const clientRegion = await this.getClient().config.region();
40394
+ assertRegionMatch(
40395
+ clientRegion,
40396
+ context?.expectedRegion,
40397
+ resourceType,
40398
+ logicalId,
40399
+ physicalId
40400
+ );
40401
+ this.logger.debug(`AutoScalingGroup ${physicalId} does not exist, skipping deletion`);
40402
+ return;
40403
+ }
40404
+ const cause = error instanceof Error ? error : void 0;
40405
+ throw new ProvisioningError(
40406
+ `Failed to delete AutoScalingGroup ${logicalId}: ${error instanceof Error ? error.message : String(error)}`,
40407
+ resourceType,
40408
+ logicalId,
40409
+ physicalId,
40410
+ cause
40411
+ );
40412
+ }
40413
+ }
40414
+ async getAttribute(physicalId, _resourceType, attributeName) {
40415
+ const group = await this.describeGroup(physicalId);
40416
+ if (!group) {
40417
+ throw new ProvisioningError(
40418
+ `AutoScalingGroup ${physicalId} not found while resolving attribute ${attributeName}`,
40419
+ "AWS::AutoScaling::AutoScalingGroup",
40420
+ physicalId,
40421
+ physicalId
40422
+ );
40423
+ }
40424
+ switch (attributeName) {
40425
+ case "Arn":
40426
+ case "AutoScalingGroupARN":
40427
+ return group.AutoScalingGroupARN ?? "";
40428
+ case "LaunchConfigurationName":
40429
+ return group.LaunchConfigurationName ?? "";
40430
+ case "LaunchTemplateID":
40431
+ case "LaunchTemplateId":
40432
+ return group.LaunchTemplate?.LaunchTemplateId ?? "";
40433
+ default:
40434
+ return "";
40435
+ }
40436
+ }
40437
+ /**
40438
+ * Read the AWS-current AutoScalingGroup configuration in CFn-property shape.
40439
+ *
40440
+ * Surfaces the user-controllable subset of `DescribeAutoScalingGroups`,
40441
+ * with always-emit placeholders on user-controllable top-level keys per
40442
+ * the cdkd PR #145 always-emit convention so that v3 `observedProperties`
40443
+ * baseline catches console-side ADDs to fields a clean deploy did not
40444
+ * template (e.g. a console-set `DeletionProtection: 'prevent-force-deletion'`
40445
+ * on a group originally created without it).
40446
+ *
40447
+ * Returns `undefined` when the group is gone.
40448
+ */
40449
+ async readCurrentState(physicalId, _logicalId, _resourceType) {
40450
+ let group;
40451
+ try {
40452
+ group = await this.describeGroup(physicalId);
40453
+ } catch (err) {
40454
+ if (this.isNotFoundError(err))
40455
+ return void 0;
40456
+ throw err;
40457
+ }
40458
+ if (!group)
40459
+ return void 0;
40460
+ const result = {};
40461
+ if (group.AutoScalingGroupName !== void 0) {
40462
+ result["AutoScalingGroupName"] = group.AutoScalingGroupName;
40463
+ }
40464
+ if (group.LaunchTemplate) {
40465
+ const lt = {};
40466
+ if (group.LaunchTemplate.LaunchTemplateId !== void 0) {
40467
+ lt["LaunchTemplateId"] = group.LaunchTemplate.LaunchTemplateId;
40468
+ }
40469
+ if (group.LaunchTemplate.LaunchTemplateName !== void 0) {
40470
+ lt["LaunchTemplateName"] = group.LaunchTemplate.LaunchTemplateName;
40471
+ }
40472
+ if (group.LaunchTemplate.Version !== void 0) {
40473
+ lt["Version"] = group.LaunchTemplate.Version;
40474
+ }
40475
+ result["LaunchTemplate"] = lt;
40476
+ }
40477
+ result["MinSize"] = group.MinSize ?? 0;
40478
+ result["MaxSize"] = group.MaxSize ?? 0;
40479
+ if (group.DesiredCapacity !== void 0)
40480
+ result["DesiredCapacity"] = group.DesiredCapacity;
40481
+ if (group.VPCZoneIdentifier !== void 0 && group.VPCZoneIdentifier !== "") {
40482
+ result["VPCZoneIdentifier"] = group.VPCZoneIdentifier.split(",").map((s) => s.trim());
40483
+ } else {
40484
+ result["VPCZoneIdentifier"] = [];
40485
+ }
40486
+ result["AvailabilityZones"] = group.AvailabilityZones ?? [];
40487
+ if (group.HealthCheckType !== void 0)
40488
+ result["HealthCheckType"] = group.HealthCheckType;
40489
+ if (group.HealthCheckGracePeriod !== void 0) {
40490
+ result["HealthCheckGracePeriod"] = group.HealthCheckGracePeriod;
40491
+ }
40492
+ if (group.DefaultCooldown !== void 0) {
40493
+ result["Cooldown"] = group.DefaultCooldown;
40494
+ }
40495
+ result["NewInstancesProtectedFromScaleIn"] = group.NewInstancesProtectedFromScaleIn ?? false;
40496
+ result["TerminationPolicies"] = group.TerminationPolicies ?? [];
40497
+ result["CapacityRebalance"] = group.CapacityRebalance ?? false;
40498
+ if (group.ServiceLinkedRoleARN !== void 0) {
40499
+ result["ServiceLinkedRoleARN"] = group.ServiceLinkedRoleARN;
40500
+ }
40501
+ if (group.MaxInstanceLifetime !== void 0) {
40502
+ result["MaxInstanceLifetime"] = group.MaxInstanceLifetime;
40503
+ }
40504
+ result["LoadBalancerNames"] = group.LoadBalancerNames ?? [];
40505
+ result["TargetGroupARNs"] = group.TargetGroupARNs ?? [];
40506
+ if (group.Context !== void 0)
40507
+ result["Context"] = group.Context;
40508
+ if (group.DesiredCapacityType !== void 0) {
40509
+ result["DesiredCapacityType"] = group.DesiredCapacityType;
40510
+ }
40511
+ if (group.DefaultInstanceWarmup !== void 0) {
40512
+ result["DefaultInstanceWarmup"] = group.DefaultInstanceWarmup;
40513
+ }
40514
+ if (group.MixedInstancesPolicy !== void 0) {
40515
+ result["MixedInstancesPolicy"] = group.MixedInstancesPolicy;
40516
+ }
40517
+ if (group.AvailabilityZoneDistribution !== void 0) {
40518
+ result["AvailabilityZoneDistribution"] = group.AvailabilityZoneDistribution;
40519
+ }
40520
+ if (group.AvailabilityZoneImpairmentPolicy !== void 0) {
40521
+ result["AvailabilityZoneImpairmentPolicy"] = group.AvailabilityZoneImpairmentPolicy;
40522
+ }
40523
+ if (group.CapacityReservationSpecification !== void 0) {
40524
+ result["CapacityReservationSpecification"] = group.CapacityReservationSpecification;
40525
+ }
40526
+ if (group.InstanceMaintenancePolicy !== void 0) {
40527
+ result["InstanceMaintenancePolicy"] = group.InstanceMaintenancePolicy;
40528
+ }
40529
+ if (group.DeletionProtection !== void 0) {
40530
+ result["DeletionProtection"] = group.DeletionProtection;
40531
+ } else {
40532
+ result["DeletionProtection"] = "none";
40533
+ }
40534
+ result["Tags"] = normalizeAwsTagsToCfn(group.Tags);
40535
+ return result;
40536
+ }
40537
+ /**
40538
+ * MetricsCollection / LifecycleHookSpecificationList / TrafficSources
40539
+ * are surfaced via separate APIs (`DescribeMetricsCollectionTypes` etc.
40540
+ * + `DescribeLifecycleHooks` + `DescribeTrafficSources`) that this v1
40541
+ * does not yet wire up — declare them as drift-unknown so the comparator
40542
+ * does not fire false-positive drift on every clean run.
40543
+ */
40544
+ getDriftUnknownPaths(_resourceType) {
40545
+ return [
40546
+ "MetricsCollection",
40547
+ "LifecycleHookSpecificationList",
40548
+ "TrafficSources",
40549
+ "NotificationConfigurations"
40550
+ ];
40551
+ }
40552
+ // ─── Helpers ──────────────────────────────────────────────────────
40553
+ buildLaunchTemplate(properties) {
40554
+ const lt = properties["LaunchTemplate"];
40555
+ if (!lt)
40556
+ return void 0;
40557
+ const out = {};
40558
+ if (lt.LaunchTemplateId !== void 0)
40559
+ out.LaunchTemplateId = lt.LaunchTemplateId;
40560
+ if (lt.LaunchTemplateName !== void 0)
40561
+ out.LaunchTemplateName = lt.LaunchTemplateName;
40562
+ if (lt.Version !== void 0)
40563
+ out.Version = lt.Version;
40564
+ if (out.LaunchTemplateId === void 0 && out.LaunchTemplateName === void 0) {
40565
+ return void 0;
40566
+ }
40567
+ return out;
40568
+ }
40569
+ /**
40570
+ * CFn `Tags` is `[{Key, Value, PropagateAtLaunch?}]`. AWS expects each
40571
+ * tag to also carry `ResourceId: <groupName>` and `ResourceType:
40572
+ * 'auto-scaling-group'`. We tack those on at create time so the SDK
40573
+ * input shape matches without forcing the user to template them.
40574
+ */
40575
+ buildTags(groupName, properties) {
40576
+ const raw = properties["Tags"];
40577
+ if (!raw)
40578
+ return [];
40579
+ return raw.filter((t) => t.Key !== void 0).map((t) => ({
40580
+ ResourceId: groupName,
40581
+ ResourceType: "auto-scaling-group",
40582
+ Key: t.Key,
40583
+ Value: t.Value ?? "",
40584
+ PropagateAtLaunch: t.PropagateAtLaunch ?? false
40585
+ }));
40586
+ }
40587
+ /**
40588
+ * CFn `VPCZoneIdentifier` is a list of subnet ids; the AWS SDK input
40589
+ * field is a comma-joined string.
40590
+ */
40591
+ joinVpcZoneIdentifier(value) {
40592
+ if (value === void 0 || value === null)
40593
+ return void 0;
40594
+ if (Array.isArray(value)) {
40595
+ const cleaned = value.map((v) => String(v).trim()).filter((v) => v.length > 0);
40596
+ if (cleaned.length === 0)
40597
+ return void 0;
40598
+ return cleaned.join(",");
40599
+ }
40600
+ if (typeof value === "string")
40601
+ return value;
40602
+ return void 0;
40603
+ }
40604
+ async describeGroup(groupName) {
40605
+ const response = await this.getClient().send(
40606
+ new DescribeAutoScalingGroupsCommand({
40607
+ AutoScalingGroupNames: [groupName]
40608
+ })
40609
+ );
40610
+ return response.AutoScalingGroups?.[0];
40611
+ }
40612
+ async fetchArn(groupName) {
40613
+ try {
40614
+ const group = await this.describeGroup(groupName);
40615
+ return group?.AutoScalingGroupARN;
40616
+ } catch (err) {
40617
+ this.logger.debug(
40618
+ `DescribeAutoScalingGroups(${groupName}) failed: ${err instanceof Error ? err.message : String(err)}`
40619
+ );
40620
+ return void 0;
40621
+ }
40622
+ }
40623
+ isNotFoundError(error) {
40624
+ if (!(error instanceof Error))
40625
+ return false;
40626
+ const name = error.name ?? "";
40627
+ const message = error.message.toLowerCase();
40628
+ return name === "ValidationError" && (message.includes("autoscalinggroup name not found") || message.includes("not found") || message.includes("does not exist"));
40629
+ }
40630
+ async waitForGroupDeleted(groupName, maxWaitMs = 9e5) {
40631
+ const startTime = Date.now();
40632
+ let delay = 5e3;
40633
+ while (Date.now() - startTime < maxWaitMs) {
40634
+ try {
40635
+ const group = await this.describeGroup(groupName);
40636
+ if (!group)
40637
+ return;
40638
+ } catch (error) {
40639
+ if (this.isNotFoundError(error))
40640
+ return;
40641
+ throw error;
40642
+ }
40643
+ await this.sleep(delay);
40644
+ delay = Math.min(delay * 2, 3e4);
40645
+ }
40646
+ throw new Error(
40647
+ `Timed out waiting for AutoScalingGroup ${groupName} to be deleted (15 minute cap)`
40648
+ );
40649
+ }
40650
+ sleep(ms) {
40651
+ return new Promise((resolve4) => setTimeout(resolve4, ms));
40652
+ }
40653
+ };
40654
+
39887
40655
  // src/provisioning/register-providers.ts
39888
40656
  function registerAllProviders(registry) {
39889
40657
  registry.register("AWS::IAM::Role", new IAMRoleProvider());
@@ -39990,6 +40758,7 @@ function registerAllProviders(registry) {
39990
40758
  registry.register("AWS::CodeBuild::Project", new CodeBuildProvider());
39991
40759
  registry.register("AWS::S3Vectors::VectorBucket", new S3VectorsProvider());
39992
40760
  registry.register("AWS::ECR::Repository", new ECRProvider());
40761
+ registry.register("AWS::AutoScaling::AutoScalingGroup", new ASGProvider());
39993
40762
  const s3TablesProvider = new S3TablesProvider();
39994
40763
  registry.register("AWS::S3Tables::TableBucket", s3TablesProvider);
39995
40764
  registry.register("AWS::S3Tables::Namespace", s3TablesProvider);
@@ -42952,6 +43721,43 @@ init_aws_clients();
42952
43721
  // src/cli/commands/destroy-runner.ts
42953
43722
  import * as readline2 from "node:readline/promises";
42954
43723
  init_aws_clients();
43724
+ var PROTECTION_PROPERTY_BY_TYPE = {
43725
+ "AWS::Logs::LogGroup": "DeletionProtectionEnabled",
43726
+ "AWS::RDS::DBInstance": "DeletionProtection",
43727
+ "AWS::RDS::DBCluster": "DeletionProtection",
43728
+ "AWS::DynamoDB::Table": "DeletionProtectionEnabled",
43729
+ "AWS::EC2::Instance": "DisableApiTermination",
43730
+ "AWS::Cognito::UserPool": "DeletionProtection",
43731
+ "AWS::AutoScaling::AutoScalingGroup": "DeletionProtection"
43732
+ };
43733
+ var PROTECTION_ACTIVE_VALUES_BY_TYPE = {
43734
+ "AWS::Cognito::UserPool": /* @__PURE__ */ new Set(["ACTIVE"]),
43735
+ "AWS::AutoScaling::AutoScalingGroup": /* @__PURE__ */ new Set(["prevent-force-deletion", "prevent-all-deletion"])
43736
+ };
43737
+ function countProtectedResources(state) {
43738
+ let count = 0;
43739
+ for (const resource of Object.values(state.resources ?? {})) {
43740
+ const propName = PROTECTION_PROPERTY_BY_TYPE[resource.resourceType];
43741
+ if (propName) {
43742
+ const recorded = resource.properties?.[propName] ?? resource.observedProperties?.[propName];
43743
+ const activeValues = PROTECTION_ACTIVE_VALUES_BY_TYPE[resource.resourceType];
43744
+ if (activeValues) {
43745
+ if (activeValues.has(recorded))
43746
+ count++;
43747
+ } else if (recorded === true) {
43748
+ count++;
43749
+ }
43750
+ continue;
43751
+ }
43752
+ if (resource.resourceType === "AWS::ElasticLoadBalancingV2::LoadBalancer") {
43753
+ const attrs = resource.properties?.["LoadBalancerAttributes"] ?? resource.observedProperties?.["LoadBalancerAttributes"];
43754
+ const enabled = attrs?.find((a) => a?.Key === "deletion_protection.enabled");
43755
+ if (enabled?.Value === "true")
43756
+ count++;
43757
+ }
43758
+ }
43759
+ return count;
43760
+ }
42955
43761
  async function runDestroyForStack(stackName, state, ctx) {
42956
43762
  const logger = getLogger();
42957
43763
  const result = {
@@ -42975,18 +43781,25 @@ Resources to be deleted (${resourceCount}):`);
42975
43781
  for (const [logicalId, resource] of Object.entries(state.resources)) {
42976
43782
  logger.info(` - ${logicalId} (${resource.resourceType})`);
42977
43783
  }
43784
+ const protectedCount = ctx.removeProtection ? countProtectedResources(state) : 0;
42978
43785
  if (!ctx.skipConfirmation) {
42979
43786
  const rl = readline2.createInterface({
42980
43787
  input: process.stdin,
42981
43788
  output: process.stdout
42982
43789
  });
42983
- const answer = await rl.question(
42984
- `
42985
- Are you sure you want to destroy stack "${stackName}" and delete all ${resourceCount} resources? (Y/n): `
42986
- );
43790
+ const prompt = ctx.removeProtection ? `
43791
+ About to destroy ${resourceCount} resources from stack "${stackName}", REMOVING DELETION PROTECTION on ${protectedCount} of them. Continue? (y/N): ` : `
43792
+ Are you sure you want to destroy stack "${stackName}" and delete all ${resourceCount} resources? (Y/n): `;
43793
+ const answer = await rl.question(prompt);
42987
43794
  rl.close();
42988
43795
  const trimmed = answer.trim().toLowerCase();
42989
- if (trimmed === "n" || trimmed === "no") {
43796
+ if (ctx.removeProtection) {
43797
+ if (trimmed !== "y" && trimmed !== "yes") {
43798
+ logger.info("Destroy cancelled");
43799
+ result.cancelled = true;
43800
+ return result;
43801
+ }
43802
+ } else if (trimmed === "n" || trimmed === "no") {
42990
43803
  logger.info("Destroy cancelled");
42991
43804
  result.cancelled = true;
42992
43805
  return result;
@@ -43093,7 +43906,11 @@ Acquiring lock for stack ${stackName}...`);
43093
43906
  logicalId,
43094
43907
  resource.physicalId,
43095
43908
  resource.resourceType,
43096
- resource.properties
43909
+ resource.properties,
43910
+ {
43911
+ ...state.region !== void 0 && { expectedRegion: state.region },
43912
+ ...ctx.removeProtection === true && { removeProtection: true }
43913
+ }
43097
43914
  );
43098
43915
  lastDeleteError = null;
43099
43916
  break;
@@ -43315,10 +44132,16 @@ Preparing to destroy stack: ${stackName}`);
43315
44132
  const synthStack = appStacks.find((s) => s.stackName === stackName);
43316
44133
  const synthRegion = synthStack?.region;
43317
44134
  if (synthStack?.terminationProtection === true) {
43318
- const err = new StackTerminationProtectionError(stackName);
43319
- logger.error(` \u2717 ${err.message}`);
43320
- totalErrors++;
43321
- continue;
44135
+ if (options.removeProtection) {
44136
+ logger.warn(
44137
+ `Stack ${stackName} has terminationProtection: true \u2014 bypassing because --remove-protection set`
44138
+ );
44139
+ } else {
44140
+ const err = new StackTerminationProtectionError(stackName);
44141
+ logger.error(` \u2717 ${err.message}`);
44142
+ totalErrors++;
44143
+ continue;
44144
+ }
43322
44145
  }
43323
44146
  let stackTargetRegion;
43324
44147
  if (refs.length === 0) {
@@ -43353,6 +44176,7 @@ Preparing to destroy stack: ${stackName}`);
43353
44176
  ...options.profile && { profile: options.profile },
43354
44177
  stateBucket,
43355
44178
  skipConfirmation: options.yes || options.force,
44179
+ removeProtection: options.removeProtection === true,
43356
44180
  ...options.resourceWarnAfter?.globalMs !== void 0 && {
43357
44181
  resourceWarnAfterMs: options.resourceWarnAfter.globalMs
43358
44182
  },
@@ -45190,6 +46014,7 @@ Preparing to destroy stack: ${stackName}${ref.region ? ` (${ref.region})` : ""}`
45190
46014
  // skipped when `options.yes` is set OR `--all` was set (the user
45191
46015
  // already accepted the batch prompt).
45192
46016
  skipConfirmation: options.yes || options.all === true,
46017
+ removeProtection: options.removeProtection === true,
45193
46018
  ...options.resourceWarnAfter?.globalMs !== void 0 && {
45194
46019
  resourceWarnAfterMs: options.resourceWarnAfter.globalMs
45195
46020
  },
@@ -45218,7 +46043,11 @@ Preparing to destroy stack: ${stackName}${ref.region ? ` (${ref.region})` : ""}`
45218
46043
  function createStateDestroyCommand() {
45219
46044
  const cmd = new Command12("destroy").description(
45220
46045
  "Destroy a stack's AWS resources and remove its state record without requiring the CDK app. For removing only the state record (keeping AWS resources intact), use 'cdkd state orphan'."
45221
- ).argument("[stacks...]", "Stack name(s) to destroy (physical CloudFormation names)").option("--all", "Destroy every stack in the state bucket", false).addOption(stackRegionOption2()).addHelpText(
46046
+ ).argument("[stacks...]", "Stack name(s) to destroy (physical CloudFormation names)").option("--all", "Destroy every stack in the state bucket", false).option(
46047
+ "--remove-protection",
46048
+ "Bypass deletion protection on protected resources by flipping the per-resource protection flag off in-place before delete. Covers AWS::Logs::LogGroup, AWS::RDS::DBInstance, AWS::RDS::DBCluster, AWS::DynamoDB::Table, AWS::EC2::Instance, and AWS::ElasticLoadBalancingV2::LoadBalancer.",
46049
+ false
46050
+ ).addOption(stackRegionOption2()).addHelpText(
45222
46051
  "after",
45223
46052
  [
45224
46053
  "",
@@ -46358,7 +47187,7 @@ function reorderArgs(argv) {
46358
47187
  }
46359
47188
  async function main() {
46360
47189
  const program = new Command14();
46361
- program.name("cdkd").description("CDK Direct - Deploy AWS CDK apps directly via SDK/Cloud Control API").version("0.58.0");
47190
+ program.name("cdkd").description("CDK Direct - Deploy AWS CDK apps directly via SDK/Cloud Control API").version("0.59.0");
46362
47191
  program.addCommand(createBootstrapCommand());
46363
47192
  program.addCommand(createSynthCommand());
46364
47193
  program.addCommand(createListCommand());