@go-to-k/cdkd 0.219.2 → 0.219.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/asg-provider-B_hrCxRx.js +790 -0
- package/dist/asg-provider-B_hrCxRx.js.map +1 -0
- package/dist/{aws-clients-DWUnLza1.js → aws-clients-pjPwZz1r.js} +2 -18
- package/dist/{aws-clients-DWUnLza1.js.map → aws-clients-pjPwZz1r.js.map} +1 -1
- package/dist/cli.js +5 -786
- package/dist/cli.js.map +1 -1
- package/dist/{deploy-engine-B6CuzOHi.js → deploy-engine-Drw_e42s.js} +13 -1485
- package/dist/deploy-engine-Drw_e42s.js.map +1 -0
- package/dist/import-helpers-wLipXr5g.js +1484 -0
- package/dist/import-helpers-wLipXr5g.js.map +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -2
- package/dist/rolldown-runtime-CjeV3_4I.js +18 -0
- package/package.json +1 -1
- package/dist/deploy-engine-B6CuzOHi.js.map +0 -1
|
@@ -0,0 +1,790 @@
|
|
|
1
|
+
import { t as __exportAll } from "./rolldown-runtime-CjeV3_4I.js";
|
|
2
|
+
import { M as getLogger, S as ResourceUpdateNotSupportedError, a as assertRegionMatch, b as ProvisioningError, o as disableInstanceApiTermination, r as normalizeAwsTagsToCfn, z as generateResourceName } from "./import-helpers-wLipXr5g.js";
|
|
3
|
+
import { EC2Client } from "@aws-sdk/client-ec2";
|
|
4
|
+
import { AttachLoadBalancerTargetGroupsCommand, AttachLoadBalancersCommand, AttachTrafficSourcesCommand, AutoScalingClient, CreateAutoScalingGroupCommand, CreateOrUpdateTagsCommand, DeleteAutoScalingGroupCommand, DeleteLifecycleHookCommand, DeleteNotificationConfigurationCommand, DeleteTagsCommand as DeleteTagsCommand$1, DescribeAutoScalingGroupsCommand, DescribeLifecycleHooksCommand, DescribeNotificationConfigurationsCommand, DescribeTrafficSourcesCommand, DetachLoadBalancerTargetGroupsCommand, DetachLoadBalancersCommand, DetachTrafficSourcesCommand, DisableMetricsCollectionCommand, EnableMetricsCollectionCommand, PutLifecycleHookCommand, PutNotificationConfigurationCommand, UpdateAutoScalingGroupCommand } from "@aws-sdk/client-auto-scaling";
|
|
5
|
+
|
|
6
|
+
//#region src/provisioning/providers/asg-provider.ts
|
|
7
|
+
var asg_provider_exports = /* @__PURE__ */ __exportAll({ ASGProvider: () => ASGProvider });
|
|
8
|
+
/**
|
|
9
|
+
* AWS Auto Scaling Provider
|
|
10
|
+
*
|
|
11
|
+
* Implements resource provisioning for `AWS::AutoScaling::AutoScalingGroup`.
|
|
12
|
+
*
|
|
13
|
+
* WHY a dedicated SDK provider (instead of CC API fallback):
|
|
14
|
+
* 1. Owns the `--remove-protection` flip-off: ASG protection has three
|
|
15
|
+
* levels (`none` / `prevent-force-deletion` / `prevent-all-deletion`)
|
|
16
|
+
* and the destroy path needs to (a) clear it via `UpdateAutoScalingGroup
|
|
17
|
+
* ({DeletionProtection: 'none'})` before the actual delete and (b) set
|
|
18
|
+
* `ForceDelete: true` on `DeleteAutoScalingGroup` so AWS terminates any
|
|
19
|
+
* running instances as part of the delete (matches the user's "I know
|
|
20
|
+
* what I'm doing" intent).
|
|
21
|
+
* 2. Faster than CC API for the common case — direct Create/Update calls
|
|
22
|
+
* with no eventual-consistency polling beyond what `DescribeAutoScaling
|
|
23
|
+
* Groups` already provides.
|
|
24
|
+
*
|
|
25
|
+
* Update has narrower coverage than create: AWS does not support modifying
|
|
26
|
+
* `AutoScalingGroupName` (immutable) — that diff still surfaces
|
|
27
|
+
* `ResourceUpdateNotSupportedError` so the caller can `cdkd deploy
|
|
28
|
+
* --replace`. The mutable fields handled in-place via
|
|
29
|
+
* `UpdateAutoScalingGroup` include MinSize / MaxSize / DesiredCapacity /
|
|
30
|
+
* VPCZoneIdentifier / HealthCheckType / HealthCheckGracePeriod /
|
|
31
|
+
* DefaultCooldown / Cooldown / NewInstancesProtectedFromScaleIn /
|
|
32
|
+
* MaxInstanceLifetime / TerminationPolicies / CapacityRebalance /
|
|
33
|
+
* ServiceLinkedRoleARN / Context / DesiredCapacityType /
|
|
34
|
+
* DefaultInstanceWarmup / AvailabilityZones / AvailabilityZoneDistribution
|
|
35
|
+
* / AvailabilityZoneImpairmentPolicy / SkipZonalShiftValidation /
|
|
36
|
+
* CapacityReservationSpecification / InstanceMaintenancePolicy /
|
|
37
|
+
* DeletionProtection / MixedInstancesPolicy / LaunchTemplate.
|
|
38
|
+
*
|
|
39
|
+
* Sub-shape diffs are applied via dedicated AWS APIs before the main
|
|
40
|
+
* `UpdateAutoScalingGroup` call:
|
|
41
|
+
* - `Tags` → `CreateOrUpdateTags` / `DeleteTags` (#475)
|
|
42
|
+
* - `LoadBalancerNames` → `AttachLoadBalancers` /
|
|
43
|
+
* `DetachLoadBalancers` (#476)
|
|
44
|
+
* - `TargetGroupARNs` → `AttachLoadBalancerTargetGroups` /
|
|
45
|
+
* `DetachLoadBalancerTargetGroups` (#476)
|
|
46
|
+
* - `MetricsCollection` → `EnableMetricsCollection` /
|
|
47
|
+
* `DisableMetricsCollection`
|
|
48
|
+
* - `LifecycleHookSpecificationList` → per-entry `PutLifecycleHook` /
|
|
49
|
+
* `DeleteLifecycleHook`
|
|
50
|
+
* - `TrafficSources` → `AttachTrafficSources` /
|
|
51
|
+
* `DetachTrafficSources`
|
|
52
|
+
* - `NotificationConfigurations` → per-topic
|
|
53
|
+
* `PutNotificationConfiguration` /
|
|
54
|
+
* `DeleteNotificationConfiguration`
|
|
55
|
+
*
|
|
56
|
+
* Each helper is a no-op when the before/after JSON is identical.
|
|
57
|
+
*/
|
|
58
|
+
var ASGProvider = class ASGProvider {
|
|
59
|
+
asgClient;
|
|
60
|
+
ec2Client;
|
|
61
|
+
providerRegion = process.env["AWS_REGION"];
|
|
62
|
+
logger = getLogger().child("ASGProvider");
|
|
63
|
+
handledProperties = new Map([["AWS::AutoScaling::AutoScalingGroup", new Set([
|
|
64
|
+
"AutoScalingGroupName",
|
|
65
|
+
"LaunchTemplate",
|
|
66
|
+
"MinSize",
|
|
67
|
+
"MaxSize",
|
|
68
|
+
"DesiredCapacity",
|
|
69
|
+
"VPCZoneIdentifier",
|
|
70
|
+
"AvailabilityZones",
|
|
71
|
+
"HealthCheckType",
|
|
72
|
+
"HealthCheckGracePeriod",
|
|
73
|
+
"Cooldown",
|
|
74
|
+
"DefaultCooldown",
|
|
75
|
+
"Tags",
|
|
76
|
+
"TerminationPolicies",
|
|
77
|
+
"NewInstancesProtectedFromScaleIn",
|
|
78
|
+
"CapacityRebalance",
|
|
79
|
+
"ServiceLinkedRoleARN",
|
|
80
|
+
"MaxInstanceLifetime",
|
|
81
|
+
"LoadBalancerNames",
|
|
82
|
+
"TargetGroupARNs",
|
|
83
|
+
"MetricsCollection",
|
|
84
|
+
"LifecycleHookSpecificationList",
|
|
85
|
+
"MixedInstancesPolicy",
|
|
86
|
+
"Context",
|
|
87
|
+
"DesiredCapacityType",
|
|
88
|
+
"DefaultInstanceWarmup",
|
|
89
|
+
"TrafficSources",
|
|
90
|
+
"NotificationConfigurations",
|
|
91
|
+
"AvailabilityZoneDistribution",
|
|
92
|
+
"AvailabilityZoneImpairmentPolicy",
|
|
93
|
+
"SkipZonalShiftValidation",
|
|
94
|
+
"CapacityReservationSpecification",
|
|
95
|
+
"InstanceMaintenancePolicy",
|
|
96
|
+
"DeletionProtection"
|
|
97
|
+
])]]);
|
|
98
|
+
unhandledByDesign = new Map([["AWS::AutoScaling::AutoScalingGroup", new Map([["LaunchConfigurationName", "AWS Launch Configurations end-of-life 2024-10; use LaunchTemplate instead"], ["NotificationConfiguration", "Legacy singular form; use NotificationConfigurations (plural) which cdkd already wires"]])]]);
|
|
99
|
+
getClient() {
|
|
100
|
+
if (!this.asgClient) this.asgClient = new AutoScalingClient(this.providerRegion ? { region: this.providerRegion } : {});
|
|
101
|
+
return this.asgClient;
|
|
102
|
+
}
|
|
103
|
+
getEc2Client() {
|
|
104
|
+
if (!this.ec2Client) this.ec2Client = new EC2Client(this.providerRegion ? { region: this.providerRegion } : {});
|
|
105
|
+
return this.ec2Client;
|
|
106
|
+
}
|
|
107
|
+
async create(logicalId, resourceType, properties) {
|
|
108
|
+
if (resourceType !== "AWS::AutoScaling::AutoScalingGroup") throw new ProvisioningError(`Unsupported resource type: ${resourceType}`, resourceType, logicalId);
|
|
109
|
+
const groupName = properties["AutoScalingGroupName"] || generateResourceName(logicalId, { maxLength: 255 });
|
|
110
|
+
this.logger.debug(`Creating AutoScalingGroup ${logicalId}: ${groupName}`);
|
|
111
|
+
try {
|
|
112
|
+
const launchTemplate = this.buildLaunchTemplate(properties);
|
|
113
|
+
const tags = this.buildTags(groupName, properties);
|
|
114
|
+
const vpcZoneIdentifier = this.joinVpcZoneIdentifier(properties["VPCZoneIdentifier"]);
|
|
115
|
+
const minSize = properties["MinSize"] != null ? Number(properties["MinSize"]) : 0;
|
|
116
|
+
const maxSize = properties["MaxSize"] != null ? Number(properties["MaxSize"]) : minSize;
|
|
117
|
+
await this.getClient().send(new CreateAutoScalingGroupCommand({
|
|
118
|
+
AutoScalingGroupName: groupName,
|
|
119
|
+
MinSize: minSize,
|
|
120
|
+
MaxSize: maxSize,
|
|
121
|
+
...properties["DesiredCapacity"] != null && { DesiredCapacity: Number(properties["DesiredCapacity"]) },
|
|
122
|
+
...launchTemplate && { LaunchTemplate: launchTemplate },
|
|
123
|
+
...properties["MixedInstancesPolicy"] !== void 0 && { MixedInstancesPolicy: properties["MixedInstancesPolicy"] },
|
|
124
|
+
...vpcZoneIdentifier !== void 0 && { VPCZoneIdentifier: vpcZoneIdentifier },
|
|
125
|
+
...properties["AvailabilityZones"] !== void 0 && { AvailabilityZones: properties["AvailabilityZones"] },
|
|
126
|
+
...properties["HealthCheckType"] !== void 0 && { HealthCheckType: properties["HealthCheckType"] },
|
|
127
|
+
...properties["HealthCheckGracePeriod"] != null && { HealthCheckGracePeriod: Number(properties["HealthCheckGracePeriod"]) },
|
|
128
|
+
...properties["Cooldown"] != null && { DefaultCooldown: Number(properties["Cooldown"]) },
|
|
129
|
+
...properties["DefaultCooldown"] != null && { DefaultCooldown: Number(properties["DefaultCooldown"]) },
|
|
130
|
+
...properties["TerminationPolicies"] !== void 0 && { TerminationPolicies: properties["TerminationPolicies"] },
|
|
131
|
+
...properties["NewInstancesProtectedFromScaleIn"] !== void 0 && { NewInstancesProtectedFromScaleIn: properties["NewInstancesProtectedFromScaleIn"] },
|
|
132
|
+
...properties["CapacityRebalance"] !== void 0 && { CapacityRebalance: properties["CapacityRebalance"] },
|
|
133
|
+
...properties["ServiceLinkedRoleARN"] !== void 0 && { ServiceLinkedRoleARN: properties["ServiceLinkedRoleARN"] },
|
|
134
|
+
...properties["MaxInstanceLifetime"] != null && { MaxInstanceLifetime: Number(properties["MaxInstanceLifetime"]) },
|
|
135
|
+
...properties["LoadBalancerNames"] !== void 0 && { LoadBalancerNames: properties["LoadBalancerNames"] },
|
|
136
|
+
...properties["TargetGroupARNs"] !== void 0 && { TargetGroupARNs: properties["TargetGroupARNs"] },
|
|
137
|
+
...properties["Context"] !== void 0 && { Context: properties["Context"] },
|
|
138
|
+
...properties["DesiredCapacityType"] !== void 0 && { DesiredCapacityType: properties["DesiredCapacityType"] },
|
|
139
|
+
...properties["DefaultInstanceWarmup"] != null && { DefaultInstanceWarmup: Number(properties["DefaultInstanceWarmup"]) },
|
|
140
|
+
...properties["LifecycleHookSpecificationList"] !== void 0 && { LifecycleHookSpecificationList: properties["LifecycleHookSpecificationList"] },
|
|
141
|
+
...properties["TrafficSources"] !== void 0 && { TrafficSources: properties["TrafficSources"] },
|
|
142
|
+
...properties["AvailabilityZoneDistribution"] !== void 0 && { AvailabilityZoneDistribution: properties["AvailabilityZoneDistribution"] },
|
|
143
|
+
...properties["AvailabilityZoneImpairmentPolicy"] !== void 0 && { AvailabilityZoneImpairmentPolicy: properties["AvailabilityZoneImpairmentPolicy"] },
|
|
144
|
+
...properties["SkipZonalShiftValidation"] !== void 0 && { SkipZonalShiftValidation: properties["SkipZonalShiftValidation"] },
|
|
145
|
+
...properties["CapacityReservationSpecification"] !== void 0 && { CapacityReservationSpecification: properties["CapacityReservationSpecification"] },
|
|
146
|
+
...properties["InstanceMaintenancePolicy"] !== void 0 && { InstanceMaintenancePolicy: properties["InstanceMaintenancePolicy"] },
|
|
147
|
+
...properties["DeletionProtection"] !== void 0 && { DeletionProtection: properties["DeletionProtection"] },
|
|
148
|
+
...tags.length > 0 && { Tags: tags }
|
|
149
|
+
}));
|
|
150
|
+
this.logger.debug(`Successfully created AutoScalingGroup ${logicalId}: ${groupName}`);
|
|
151
|
+
const arn = await this.fetchArn(groupName);
|
|
152
|
+
const attributes = {};
|
|
153
|
+
if (arn) attributes["Arn"] = arn;
|
|
154
|
+
if (launchTemplate?.LaunchTemplateId) attributes["LaunchTemplateID"] = launchTemplate.LaunchTemplateId;
|
|
155
|
+
return {
|
|
156
|
+
physicalId: groupName,
|
|
157
|
+
attributes
|
|
158
|
+
};
|
|
159
|
+
} catch (error) {
|
|
160
|
+
const cause = error instanceof Error ? error : void 0;
|
|
161
|
+
throw new ProvisioningError(`Failed to create AutoScalingGroup ${logicalId}: ${error instanceof Error ? error.message : String(error)}`, resourceType, logicalId, groupName, cause);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
async update(logicalId, physicalId, resourceType, properties, previousProperties) {
|
|
165
|
+
if (resourceType !== "AWS::AutoScaling::AutoScalingGroup") throw new ProvisioningError(`Unsupported resource type: ${resourceType}`, resourceType, logicalId, physicalId);
|
|
166
|
+
this.logger.debug(`Updating AutoScalingGroup ${logicalId}: ${physicalId}`);
|
|
167
|
+
const stringEq = (a, b) => JSON.stringify(a) === JSON.stringify(b);
|
|
168
|
+
if (!stringEq(properties["AutoScalingGroupName"], previousProperties["AutoScalingGroupName"])) throw new ResourceUpdateNotSupportedError(resourceType, logicalId, "AutoScalingGroupName is immutable on AWS — UpdateAutoScalingGroup does not accept a new name; the name is fixed at creation. Use cdkd deploy --replace to replace the group.");
|
|
169
|
+
try {
|
|
170
|
+
await this.applyTagsDiff(physicalId, properties["Tags"], previousProperties["Tags"]);
|
|
171
|
+
await this.applyLoadBalancerNamesDiff(physicalId, properties["LoadBalancerNames"], previousProperties["LoadBalancerNames"]);
|
|
172
|
+
await this.applyTargetGroupArnsDiff(physicalId, properties["TargetGroupARNs"], previousProperties["TargetGroupARNs"]);
|
|
173
|
+
await this.applyMetricsCollectionDiff(physicalId, properties["MetricsCollection"], previousProperties["MetricsCollection"]);
|
|
174
|
+
await this.applyLifecycleHooksDiff(physicalId, properties["LifecycleHookSpecificationList"], previousProperties["LifecycleHookSpecificationList"]);
|
|
175
|
+
await this.applyTrafficSourcesDiff(physicalId, properties["TrafficSources"], previousProperties["TrafficSources"]);
|
|
176
|
+
await this.applyNotificationConfigurationsDiff(physicalId, properties["NotificationConfigurations"], previousProperties["NotificationConfigurations"]);
|
|
177
|
+
const launchTemplate = this.buildLaunchTemplate(properties);
|
|
178
|
+
const vpcZoneIdentifier = this.joinVpcZoneIdentifier(properties["VPCZoneIdentifier"]);
|
|
179
|
+
await this.getClient().send(new UpdateAutoScalingGroupCommand({
|
|
180
|
+
AutoScalingGroupName: physicalId,
|
|
181
|
+
...properties["MinSize"] != null && { MinSize: Number(properties["MinSize"]) },
|
|
182
|
+
...properties["MaxSize"] != null && { MaxSize: Number(properties["MaxSize"]) },
|
|
183
|
+
...properties["DesiredCapacity"] != null && { DesiredCapacity: Number(properties["DesiredCapacity"]) },
|
|
184
|
+
...launchTemplate && { LaunchTemplate: launchTemplate },
|
|
185
|
+
...properties["MixedInstancesPolicy"] !== void 0 && { MixedInstancesPolicy: properties["MixedInstancesPolicy"] },
|
|
186
|
+
...vpcZoneIdentifier !== void 0 && { VPCZoneIdentifier: vpcZoneIdentifier },
|
|
187
|
+
...properties["AvailabilityZones"] !== void 0 && { AvailabilityZones: properties["AvailabilityZones"] },
|
|
188
|
+
...properties["HealthCheckType"] !== void 0 && { HealthCheckType: properties["HealthCheckType"] },
|
|
189
|
+
...properties["HealthCheckGracePeriod"] != null && { HealthCheckGracePeriod: Number(properties["HealthCheckGracePeriod"]) },
|
|
190
|
+
...properties["Cooldown"] != null && { DefaultCooldown: Number(properties["Cooldown"]) },
|
|
191
|
+
...properties["DefaultCooldown"] != null && { DefaultCooldown: Number(properties["DefaultCooldown"]) },
|
|
192
|
+
...properties["TerminationPolicies"] !== void 0 && { TerminationPolicies: properties["TerminationPolicies"] },
|
|
193
|
+
...properties["NewInstancesProtectedFromScaleIn"] !== void 0 && { NewInstancesProtectedFromScaleIn: properties["NewInstancesProtectedFromScaleIn"] },
|
|
194
|
+
...properties["CapacityRebalance"] !== void 0 && { CapacityRebalance: properties["CapacityRebalance"] },
|
|
195
|
+
...properties["ServiceLinkedRoleARN"] !== void 0 && { ServiceLinkedRoleARN: properties["ServiceLinkedRoleARN"] },
|
|
196
|
+
...properties["MaxInstanceLifetime"] != null && { MaxInstanceLifetime: Number(properties["MaxInstanceLifetime"]) },
|
|
197
|
+
...properties["Context"] !== void 0 && { Context: properties["Context"] },
|
|
198
|
+
...properties["DesiredCapacityType"] !== void 0 && { DesiredCapacityType: properties["DesiredCapacityType"] },
|
|
199
|
+
...properties["DefaultInstanceWarmup"] != null && { DefaultInstanceWarmup: Number(properties["DefaultInstanceWarmup"]) },
|
|
200
|
+
...properties["AvailabilityZoneDistribution"] !== void 0 && { AvailabilityZoneDistribution: properties["AvailabilityZoneDistribution"] },
|
|
201
|
+
...properties["AvailabilityZoneImpairmentPolicy"] !== void 0 && { AvailabilityZoneImpairmentPolicy: properties["AvailabilityZoneImpairmentPolicy"] },
|
|
202
|
+
...properties["SkipZonalShiftValidation"] !== void 0 && { SkipZonalShiftValidation: properties["SkipZonalShiftValidation"] },
|
|
203
|
+
...properties["CapacityReservationSpecification"] !== void 0 && { CapacityReservationSpecification: properties["CapacityReservationSpecification"] },
|
|
204
|
+
...properties["InstanceMaintenancePolicy"] !== void 0 && { InstanceMaintenancePolicy: properties["InstanceMaintenancePolicy"] },
|
|
205
|
+
...properties["DeletionProtection"] !== void 0 && { DeletionProtection: properties["DeletionProtection"] }
|
|
206
|
+
}));
|
|
207
|
+
this.logger.debug(`Successfully updated AutoScalingGroup ${logicalId}`);
|
|
208
|
+
const arn = await this.fetchArn(physicalId);
|
|
209
|
+
const attributes = {};
|
|
210
|
+
if (arn) attributes["Arn"] = arn;
|
|
211
|
+
if (launchTemplate?.LaunchTemplateId) attributes["LaunchTemplateID"] = launchTemplate.LaunchTemplateId;
|
|
212
|
+
return {
|
|
213
|
+
physicalId,
|
|
214
|
+
wasReplaced: false,
|
|
215
|
+
attributes
|
|
216
|
+
};
|
|
217
|
+
} catch (error) {
|
|
218
|
+
if (error instanceof ResourceUpdateNotSupportedError) throw error;
|
|
219
|
+
const cause = error instanceof Error ? error : void 0;
|
|
220
|
+
throw new ProvisioningError(`Failed to update AutoScalingGroup ${logicalId}: ${error instanceof Error ? error.message : String(error)}`, resourceType, logicalId, physicalId, cause);
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
async delete(logicalId, physicalId, resourceType, _properties, context) {
|
|
224
|
+
this.logger.debug(`Deleting AutoScalingGroup ${logicalId}: ${physicalId}`);
|
|
225
|
+
if (context?.removeProtection === true) {
|
|
226
|
+
try {
|
|
227
|
+
await this.getClient().send(new UpdateAutoScalingGroupCommand({
|
|
228
|
+
AutoScalingGroupName: physicalId,
|
|
229
|
+
DeletionProtection: "none"
|
|
230
|
+
}));
|
|
231
|
+
this.logger.debug(`Disabled DeletionProtection on AutoScalingGroup ${logicalId} before delete`);
|
|
232
|
+
} catch (flipError) {
|
|
233
|
+
this.logger.debug(`Could not disable DeletionProtection on ${physicalId}: ${flipError instanceof Error ? flipError.message : String(flipError)}`);
|
|
234
|
+
}
|
|
235
|
+
await this.removeInstanceTerminationProtection(physicalId, logicalId);
|
|
236
|
+
}
|
|
237
|
+
try {
|
|
238
|
+
await this.getClient().send(new DeleteAutoScalingGroupCommand({
|
|
239
|
+
AutoScalingGroupName: physicalId,
|
|
240
|
+
ForceDelete: context?.removeProtection === true
|
|
241
|
+
}));
|
|
242
|
+
this.logger.debug(`Successfully initiated deletion of AutoScalingGroup ${logicalId}`);
|
|
243
|
+
await this.waitForGroupDeleted(physicalId);
|
|
244
|
+
} catch (error) {
|
|
245
|
+
if (this.isNotFoundError(error)) {
|
|
246
|
+
assertRegionMatch(await this.getClient().config.region(), context?.expectedRegion, resourceType, logicalId, physicalId);
|
|
247
|
+
this.logger.debug(`AutoScalingGroup ${physicalId} does not exist, skipping deletion`);
|
|
248
|
+
return;
|
|
249
|
+
}
|
|
250
|
+
const cause = error instanceof Error ? error : void 0;
|
|
251
|
+
throw new ProvisioningError(`Failed to delete AutoScalingGroup ${logicalId}: ${error instanceof Error ? error.message : String(error)}`, resourceType, logicalId, physicalId, cause);
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
async getAttribute(physicalId, _resourceType, attributeName) {
|
|
255
|
+
const group = await this.describeGroup(physicalId);
|
|
256
|
+
if (!group) throw new ProvisioningError(`AutoScalingGroup ${physicalId} not found while resolving attribute ${attributeName}`, "AWS::AutoScaling::AutoScalingGroup", physicalId, physicalId);
|
|
257
|
+
switch (attributeName) {
|
|
258
|
+
case "Arn":
|
|
259
|
+
case "AutoScalingGroupARN": return group.AutoScalingGroupARN ?? "";
|
|
260
|
+
case "LaunchConfigurationName": return group.LaunchConfigurationName ?? "";
|
|
261
|
+
case "LaunchTemplateID":
|
|
262
|
+
case "LaunchTemplateId": return group.LaunchTemplate?.LaunchTemplateId ?? "";
|
|
263
|
+
default: return "";
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
/**
|
|
267
|
+
* Read the AWS-current AutoScalingGroup configuration in CFn-property shape.
|
|
268
|
+
*
|
|
269
|
+
* Surfaces the user-controllable subset of `DescribeAutoScalingGroups`,
|
|
270
|
+
* with always-emit placeholders on user-controllable top-level keys per
|
|
271
|
+
* the cdkd PR #145 always-emit convention so that v3 `observedProperties`
|
|
272
|
+
* baseline catches console-side ADDs to fields a clean deploy did not
|
|
273
|
+
* template (e.g. a console-set `DeletionProtection: 'prevent-force-deletion'`
|
|
274
|
+
* on a group originally created without it).
|
|
275
|
+
*
|
|
276
|
+
* Sub-shapes (LifecycleHookSpecificationList / TrafficSources /
|
|
277
|
+
* NotificationConfigurations) are surfaced via three parallel Describe
|
|
278
|
+
* calls fired alongside the primary `DescribeAutoScalingGroups`. Each is
|
|
279
|
+
* best-effort: a per-call failure (e.g. permissions gap on
|
|
280
|
+
* `autoscaling:DescribeLifecycleHooks`) is logged at debug and the
|
|
281
|
+
* matching key falls back to its always-emit `[]` placeholder rather
|
|
282
|
+
* than aborting the whole drift read.
|
|
283
|
+
*
|
|
284
|
+
* `MetricsCollection` is reverse-mapped from `EnabledMetrics` (already
|
|
285
|
+
* present on the primary `DescribeAutoScalingGroups` response, so no
|
|
286
|
+
* extra call is needed).
|
|
287
|
+
*
|
|
288
|
+
* Returns `undefined` when the group is gone.
|
|
289
|
+
*/
|
|
290
|
+
async readCurrentState(physicalId, _logicalId, _resourceType) {
|
|
291
|
+
const groupPromise = (async () => {
|
|
292
|
+
try {
|
|
293
|
+
return await this.describeGroup(physicalId);
|
|
294
|
+
} catch (err) {
|
|
295
|
+
if (this.isNotFoundError(err)) return void 0;
|
|
296
|
+
throw err;
|
|
297
|
+
}
|
|
298
|
+
})();
|
|
299
|
+
const lifecycleHooksPromise = this.getClient().send(new DescribeLifecycleHooksCommand({ AutoScalingGroupName: physicalId })).then((r) => r.LifecycleHooks ?? []).catch((err) => {
|
|
300
|
+
this.logger.debug(`DescribeLifecycleHooks(${physicalId}) failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
301
|
+
return [];
|
|
302
|
+
});
|
|
303
|
+
const trafficSourcesPromise = this.getClient().send(new DescribeTrafficSourcesCommand({ AutoScalingGroupName: physicalId })).then((r) => r.TrafficSources ?? []).catch((err) => {
|
|
304
|
+
this.logger.debug(`DescribeTrafficSources(${physicalId}) failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
305
|
+
return [];
|
|
306
|
+
});
|
|
307
|
+
const notificationsPromise = this.getClient().send(new DescribeNotificationConfigurationsCommand({ AutoScalingGroupNames: [physicalId] })).then((r) => r.NotificationConfigurations ?? []).catch((err) => {
|
|
308
|
+
this.logger.debug(`DescribeNotificationConfigurations(${physicalId}) failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
309
|
+
return [];
|
|
310
|
+
});
|
|
311
|
+
const [group, lifecycleHooks, trafficSources, notifications] = await Promise.all([
|
|
312
|
+
groupPromise,
|
|
313
|
+
lifecycleHooksPromise,
|
|
314
|
+
trafficSourcesPromise,
|
|
315
|
+
notificationsPromise
|
|
316
|
+
]);
|
|
317
|
+
if (!group) return void 0;
|
|
318
|
+
const result = {};
|
|
319
|
+
if (group.AutoScalingGroupName !== void 0) result["AutoScalingGroupName"] = group.AutoScalingGroupName;
|
|
320
|
+
if (group.LaunchTemplate) {
|
|
321
|
+
const lt = {};
|
|
322
|
+
if (group.LaunchTemplate.LaunchTemplateId !== void 0) lt["LaunchTemplateId"] = group.LaunchTemplate.LaunchTemplateId;
|
|
323
|
+
if (group.LaunchTemplate.LaunchTemplateName !== void 0) lt["LaunchTemplateName"] = group.LaunchTemplate.LaunchTemplateName;
|
|
324
|
+
if (group.LaunchTemplate.Version !== void 0) lt["Version"] = group.LaunchTemplate.Version;
|
|
325
|
+
result["LaunchTemplate"] = lt;
|
|
326
|
+
}
|
|
327
|
+
result["MinSize"] = group.MinSize ?? 0;
|
|
328
|
+
result["MaxSize"] = group.MaxSize ?? 0;
|
|
329
|
+
if (group.DesiredCapacity !== void 0) result["DesiredCapacity"] = group.DesiredCapacity;
|
|
330
|
+
if (group.VPCZoneIdentifier !== void 0 && group.VPCZoneIdentifier !== "") result["VPCZoneIdentifier"] = group.VPCZoneIdentifier.split(",").map((s) => s.trim());
|
|
331
|
+
else result["VPCZoneIdentifier"] = [];
|
|
332
|
+
result["AvailabilityZones"] = group.AvailabilityZones ?? [];
|
|
333
|
+
if (group.HealthCheckType !== void 0) result["HealthCheckType"] = group.HealthCheckType;
|
|
334
|
+
if (group.HealthCheckGracePeriod !== void 0) result["HealthCheckGracePeriod"] = group.HealthCheckGracePeriod;
|
|
335
|
+
if (group.DefaultCooldown !== void 0) result["Cooldown"] = group.DefaultCooldown;
|
|
336
|
+
result["NewInstancesProtectedFromScaleIn"] = group.NewInstancesProtectedFromScaleIn ?? false;
|
|
337
|
+
result["TerminationPolicies"] = group.TerminationPolicies ?? [];
|
|
338
|
+
result["CapacityRebalance"] = group.CapacityRebalance ?? false;
|
|
339
|
+
if (group.ServiceLinkedRoleARN !== void 0) result["ServiceLinkedRoleARN"] = group.ServiceLinkedRoleARN;
|
|
340
|
+
if (group.MaxInstanceLifetime !== void 0) result["MaxInstanceLifetime"] = group.MaxInstanceLifetime;
|
|
341
|
+
result["LoadBalancerNames"] = group.LoadBalancerNames ?? [];
|
|
342
|
+
result["TargetGroupARNs"] = group.TargetGroupARNs ?? [];
|
|
343
|
+
if (group.Context !== void 0) result["Context"] = group.Context;
|
|
344
|
+
if (group.DesiredCapacityType !== void 0) result["DesiredCapacityType"] = group.DesiredCapacityType;
|
|
345
|
+
if (group.DefaultInstanceWarmup !== void 0) result["DefaultInstanceWarmup"] = group.DefaultInstanceWarmup;
|
|
346
|
+
if (group.MixedInstancesPolicy !== void 0) result["MixedInstancesPolicy"] = group.MixedInstancesPolicy;
|
|
347
|
+
if (group.AvailabilityZoneDistribution !== void 0) result["AvailabilityZoneDistribution"] = group.AvailabilityZoneDistribution;
|
|
348
|
+
if (group.AvailabilityZoneImpairmentPolicy !== void 0) result["AvailabilityZoneImpairmentPolicy"] = group.AvailabilityZoneImpairmentPolicy;
|
|
349
|
+
if (group.CapacityReservationSpecification !== void 0) result["CapacityReservationSpecification"] = group.CapacityReservationSpecification;
|
|
350
|
+
if (group.InstanceMaintenancePolicy !== void 0) result["InstanceMaintenancePolicy"] = group.InstanceMaintenancePolicy;
|
|
351
|
+
if (group.DeletionProtection !== void 0) result["DeletionProtection"] = group.DeletionProtection;
|
|
352
|
+
else result["DeletionProtection"] = "none";
|
|
353
|
+
result["Tags"] = normalizeAwsTagsToCfn(group.Tags);
|
|
354
|
+
result["MetricsCollection"] = mapEnabledMetricsToCfn(group.EnabledMetrics);
|
|
355
|
+
result["LifecycleHookSpecificationList"] = mapLifecycleHooksToCfn(lifecycleHooks);
|
|
356
|
+
result["TrafficSources"] = mapTrafficSourcesToCfn(trafficSources.filter((t) => {
|
|
357
|
+
if (t.Identifier === void 0) return false;
|
|
358
|
+
if (t.Type === "elbv2" || t.Type === "elb") return false;
|
|
359
|
+
return true;
|
|
360
|
+
}));
|
|
361
|
+
result["NotificationConfigurations"] = mapNotificationsToCfn(notifications);
|
|
362
|
+
return result;
|
|
363
|
+
}
|
|
364
|
+
buildLaunchTemplate(properties) {
|
|
365
|
+
const lt = properties["LaunchTemplate"];
|
|
366
|
+
if (!lt) return void 0;
|
|
367
|
+
const out = {};
|
|
368
|
+
if (lt.LaunchTemplateId !== void 0) {
|
|
369
|
+
out.LaunchTemplateId = lt.LaunchTemplateId;
|
|
370
|
+
if (lt.LaunchTemplateName !== void 0) this.logger.debug(`buildLaunchTemplate: both LaunchTemplateId (${lt.LaunchTemplateId}) and LaunchTemplateName (${lt.LaunchTemplateName}) templated; dropping Name (#551)`);
|
|
371
|
+
} else if (lt.LaunchTemplateName !== void 0) out.LaunchTemplateName = lt.LaunchTemplateName;
|
|
372
|
+
if (lt.Version !== void 0) out.Version = String(lt.Version);
|
|
373
|
+
if (out.LaunchTemplateId === void 0 && out.LaunchTemplateName === void 0) return;
|
|
374
|
+
return out;
|
|
375
|
+
}
|
|
376
|
+
/**
|
|
377
|
+
* CFn `Tags` is `[{Key, Value, PropagateAtLaunch?}]`. AWS expects each
|
|
378
|
+
* tag to also carry `ResourceId: <groupName>` and `ResourceType:
|
|
379
|
+
* 'auto-scaling-group'`. We tack those on at create time so the SDK
|
|
380
|
+
* input shape matches without forcing the user to template them.
|
|
381
|
+
*/
|
|
382
|
+
buildTags(groupName, properties) {
|
|
383
|
+
const raw = properties["Tags"];
|
|
384
|
+
if (!raw) return [];
|
|
385
|
+
return raw.filter((t) => t.Key !== void 0).map((t) => ({
|
|
386
|
+
ResourceId: groupName,
|
|
387
|
+
ResourceType: "auto-scaling-group",
|
|
388
|
+
Key: t.Key,
|
|
389
|
+
Value: t.Value ?? "",
|
|
390
|
+
PropagateAtLaunch: t.PropagateAtLaunch ?? false
|
|
391
|
+
}));
|
|
392
|
+
}
|
|
393
|
+
/**
|
|
394
|
+
* CFn `VPCZoneIdentifier` is a list of subnet ids; the AWS SDK input
|
|
395
|
+
* field is a comma-joined string.
|
|
396
|
+
*/
|
|
397
|
+
joinVpcZoneIdentifier(value) {
|
|
398
|
+
if (value === void 0 || value === null) return void 0;
|
|
399
|
+
if (Array.isArray(value)) {
|
|
400
|
+
const cleaned = value.map((v) => String(v).trim()).filter((v) => v.length > 0);
|
|
401
|
+
if (cleaned.length === 0) return void 0;
|
|
402
|
+
return cleaned.join(",");
|
|
403
|
+
}
|
|
404
|
+
if (typeof value === "string") return value;
|
|
405
|
+
}
|
|
406
|
+
async describeGroup(groupName) {
|
|
407
|
+
return (await this.getClient().send(new DescribeAutoScalingGroupsCommand({ AutoScalingGroupNames: [groupName] }))).AutoScalingGroups?.[0];
|
|
408
|
+
}
|
|
409
|
+
/**
|
|
410
|
+
* Flip EC2-level termination protection (`DisableApiTermination`) off on
|
|
411
|
+
* every instance currently launched by the group, so the subsequent
|
|
412
|
+
* `DeleteAutoScalingGroup(ForceDelete: true)` can actually terminate them
|
|
413
|
+
* instead of orphaning the protected instances (issue #796). Best-effort:
|
|
414
|
+
* a Describe failure or a per-instance flip failure is logged at debug and
|
|
415
|
+
* does not block the delete (the modify WRITE lags the terminate READ, so
|
|
416
|
+
* the shared helper swallows propagation errors the same way the EC2 path
|
|
417
|
+
* does — the orphan, if any, surfaces as a leftover instance the caller
|
|
418
|
+
* can clean up rather than a hard delete failure).
|
|
419
|
+
*/
|
|
420
|
+
async removeInstanceTerminationProtection(groupName, logicalId) {
|
|
421
|
+
let instanceIds;
|
|
422
|
+
try {
|
|
423
|
+
instanceIds = ((await this.describeGroup(groupName))?.Instances ?? []).map((i) => i.InstanceId).filter((id) => typeof id === "string" && id.length > 0);
|
|
424
|
+
} catch (describeError) {
|
|
425
|
+
this.logger.debug(`Could not enumerate instances of AutoScalingGroup ${logicalId} for termination-protection removal: ${describeError instanceof Error ? describeError.message : String(describeError)}`);
|
|
426
|
+
return;
|
|
427
|
+
}
|
|
428
|
+
if (instanceIds.length === 0) return;
|
|
429
|
+
this.logger.debug(`Disabling EC2 termination protection on ${instanceIds.length} instance(s) of AutoScalingGroup ${logicalId} before force delete`);
|
|
430
|
+
for (const instanceId of instanceIds) await disableInstanceApiTermination(this.getEc2Client(), instanceId, this.logger);
|
|
431
|
+
}
|
|
432
|
+
async fetchArn(groupName) {
|
|
433
|
+
try {
|
|
434
|
+
return (await this.describeGroup(groupName))?.AutoScalingGroupARN;
|
|
435
|
+
} catch (err) {
|
|
436
|
+
this.logger.debug(`DescribeAutoScalingGroups(${groupName}) failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
437
|
+
return;
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
isNotFoundError(error) {
|
|
441
|
+
if (!(error instanceof Error)) return false;
|
|
442
|
+
const name = error.name ?? "";
|
|
443
|
+
const message = error.message.toLowerCase();
|
|
444
|
+
return name === "ValidationError" && (message.includes("autoscalinggroup name not found") || message.includes("not found") || message.includes("does not exist"));
|
|
445
|
+
}
|
|
446
|
+
async waitForGroupDeleted(groupName, maxWaitMs = 9e5) {
|
|
447
|
+
const startTime = Date.now();
|
|
448
|
+
let delay = 5e3;
|
|
449
|
+
while (Date.now() - startTime < maxWaitMs) {
|
|
450
|
+
try {
|
|
451
|
+
if (!await this.describeGroup(groupName)) return;
|
|
452
|
+
} catch (error) {
|
|
453
|
+
if (this.isNotFoundError(error)) return;
|
|
454
|
+
throw error;
|
|
455
|
+
}
|
|
456
|
+
await this.sleep(delay);
|
|
457
|
+
delay = Math.min(delay * 2, 3e4);
|
|
458
|
+
}
|
|
459
|
+
throw new Error(`Timed out waiting for AutoScalingGroup ${groupName} to be deleted (15 minute cap)`);
|
|
460
|
+
}
|
|
461
|
+
sleep(ms) {
|
|
462
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
463
|
+
}
|
|
464
|
+
/**
|
|
465
|
+
* Diff and apply changes to the ASG's `Tags` property via the
|
|
466
|
+
* `CreateOrUpdateTags` / `DeleteTags` AWS APIs (#475). CFn Tags shape is
|
|
467
|
+
* `[{Key, Value, PropagateAtLaunch}]`; AWS Tag input adds `ResourceId`
|
|
468
|
+
* (= the ASG name) and `ResourceType: 'auto-scaling-group'`.
|
|
469
|
+
*
|
|
470
|
+
* Diff semantics:
|
|
471
|
+
* - Removed keys → `DeleteTags`.
|
|
472
|
+
* - Added keys → `CreateOrUpdateTags`.
|
|
473
|
+
* - Modified value or `PropagateAtLaunch` flag → `CreateOrUpdateTags`
|
|
474
|
+
* (the AWS API upserts by `(ResourceId, ResourceType, Key)` tuple, so
|
|
475
|
+
* a single upsert call replaces the old value).
|
|
476
|
+
*
|
|
477
|
+
* No-op when before/after JSON is identical.
|
|
478
|
+
*/
|
|
479
|
+
async applyTagsDiff(physicalId, next, prev) {
|
|
480
|
+
if (JSON.stringify(next ?? []) === JSON.stringify(prev ?? [])) return;
|
|
481
|
+
const nextEntries = Array.isArray(next) ? next : [];
|
|
482
|
+
const prevEntries = Array.isArray(prev) ? prev : [];
|
|
483
|
+
const nextByKey = /* @__PURE__ */ new Map();
|
|
484
|
+
for (const t of nextEntries) if (t.Key) nextByKey.set(t.Key, t);
|
|
485
|
+
const prevByKey = /* @__PURE__ */ new Map();
|
|
486
|
+
for (const t of prevEntries) if (t.Key) prevByKey.set(t.Key, t);
|
|
487
|
+
const toDelete = [];
|
|
488
|
+
for (const [key, tag] of prevByKey) if (!nextByKey.has(key)) toDelete.push(tag);
|
|
489
|
+
if (toDelete.length > 0) await this.getClient().send(new DeleteTagsCommand$1({ Tags: toDelete.map((t) => ({
|
|
490
|
+
ResourceId: physicalId,
|
|
491
|
+
ResourceType: "auto-scaling-group",
|
|
492
|
+
Key: t.Key
|
|
493
|
+
})) }));
|
|
494
|
+
const toUpsert = [];
|
|
495
|
+
for (const [key, tag] of nextByKey) {
|
|
496
|
+
const before = prevByKey.get(key);
|
|
497
|
+
if (JSON.stringify(before) === JSON.stringify(tag)) continue;
|
|
498
|
+
toUpsert.push(tag);
|
|
499
|
+
}
|
|
500
|
+
if (toUpsert.length > 0) await this.getClient().send(new CreateOrUpdateTagsCommand({ Tags: toUpsert.map((t) => ({
|
|
501
|
+
ResourceId: physicalId,
|
|
502
|
+
ResourceType: "auto-scaling-group",
|
|
503
|
+
Key: t.Key,
|
|
504
|
+
...t.Value !== void 0 && { Value: t.Value },
|
|
505
|
+
...t.PropagateAtLaunch !== void 0 && { PropagateAtLaunch: t.PropagateAtLaunch }
|
|
506
|
+
})) }));
|
|
507
|
+
}
|
|
508
|
+
/**
|
|
509
|
+
* Diff `LoadBalancerNames` (Classic Load Balancers) and issue
|
|
510
|
+
* `AttachLoadBalancers` / `DetachLoadBalancers` for the delta (#476).
|
|
511
|
+
* Names are opaque strings; AWS allows N attached LBs per ASG so this
|
|
512
|
+
* helper batches every add into one Attach call and every remove into
|
|
513
|
+
* one Detach call. No-op when before/after JSON is identical.
|
|
514
|
+
*/
|
|
515
|
+
async applyLoadBalancerNamesDiff(physicalId, next, prev) {
|
|
516
|
+
if (JSON.stringify(next ?? []) === JSON.stringify(prev ?? [])) return;
|
|
517
|
+
const nextNames = (Array.isArray(next) ? next : []).filter((n) => typeof n === "string");
|
|
518
|
+
const prevNames = (Array.isArray(prev) ? prev : []).filter((n) => typeof n === "string");
|
|
519
|
+
const nextSet = new Set(nextNames);
|
|
520
|
+
const prevSet = new Set(prevNames);
|
|
521
|
+
const toAttach = nextNames.filter((n) => !prevSet.has(n));
|
|
522
|
+
const toDetach = prevNames.filter((n) => !nextSet.has(n));
|
|
523
|
+
if (toDetach.length > 0) await this.getClient().send(new DetachLoadBalancersCommand({
|
|
524
|
+
AutoScalingGroupName: physicalId,
|
|
525
|
+
LoadBalancerNames: toDetach
|
|
526
|
+
}));
|
|
527
|
+
if (toAttach.length > 0) await this.getClient().send(new AttachLoadBalancersCommand({
|
|
528
|
+
AutoScalingGroupName: physicalId,
|
|
529
|
+
LoadBalancerNames: toAttach
|
|
530
|
+
}));
|
|
531
|
+
}
|
|
532
|
+
/**
|
|
533
|
+
* Diff `TargetGroupARNs` (ALB / NLB target groups) and issue
|
|
534
|
+
* `AttachLoadBalancerTargetGroups` /
|
|
535
|
+
* `DetachLoadBalancerTargetGroups` for the delta (#476). Target-group
|
|
536
|
+
* ARNs are opaque strings; same per-call batching pattern as
|
|
537
|
+
* `applyLoadBalancerNamesDiff`. No-op when before/after JSON is
|
|
538
|
+
* identical.
|
|
539
|
+
*/
|
|
540
|
+
async applyTargetGroupArnsDiff(physicalId, next, prev) {
|
|
541
|
+
if (JSON.stringify(next ?? []) === JSON.stringify(prev ?? [])) return;
|
|
542
|
+
const nextArns = (Array.isArray(next) ? next : []).filter((a) => typeof a === "string");
|
|
543
|
+
const prevArns = (Array.isArray(prev) ? prev : []).filter((a) => typeof a === "string");
|
|
544
|
+
const nextSet = new Set(nextArns);
|
|
545
|
+
const prevSet = new Set(prevArns);
|
|
546
|
+
const toAttach = nextArns.filter((a) => !prevSet.has(a));
|
|
547
|
+
const toDetach = prevArns.filter((a) => !nextSet.has(a));
|
|
548
|
+
if (toDetach.length > 0) await this.getClient().send(new DetachLoadBalancerTargetGroupsCommand({
|
|
549
|
+
AutoScalingGroupName: physicalId,
|
|
550
|
+
TargetGroupARNs: toDetach
|
|
551
|
+
}));
|
|
552
|
+
if (toAttach.length > 0) await this.getClient().send(new AttachLoadBalancerTargetGroupsCommand({
|
|
553
|
+
AutoScalingGroupName: physicalId,
|
|
554
|
+
TargetGroupARNs: toAttach
|
|
555
|
+
}));
|
|
556
|
+
if (toDetach.length > 0 || toAttach.length > 0) await this.waitForTargetGroupArnsConvergence(physicalId, new Set(nextArns));
|
|
557
|
+
}
|
|
558
|
+
static TG_CONVERGENCE_TIMEOUT_MS = 3e4;
|
|
559
|
+
static TG_CONVERGENCE_POLL_INTERVAL_MS = 1e3;
|
|
560
|
+
async waitForTargetGroupArnsConvergence(physicalId, expected) {
|
|
561
|
+
const deadlineMs = Date.now() + ASGProvider.TG_CONVERGENCE_TIMEOUT_MS;
|
|
562
|
+
let lastObserved = /* @__PURE__ */ new Set();
|
|
563
|
+
while (Date.now() < deadlineMs) {
|
|
564
|
+
let resp;
|
|
565
|
+
try {
|
|
566
|
+
resp = await this.getClient().send(new DescribeAutoScalingGroupsCommand({ AutoScalingGroupNames: [physicalId] }));
|
|
567
|
+
} catch (err) {
|
|
568
|
+
this.logger.debug(`applyTargetGroupArnsDiff convergence poll: transient error, retrying — ${err instanceof Error ? err.message : String(err)}`);
|
|
569
|
+
await new Promise((r) => setTimeout(r, ASGProvider.TG_CONVERGENCE_POLL_INTERVAL_MS));
|
|
570
|
+
continue;
|
|
571
|
+
}
|
|
572
|
+
lastObserved = new Set(resp.AutoScalingGroups?.[0]?.TargetGroupARNs ?? []);
|
|
573
|
+
if (lastObserved.size === expected.size && [...expected].every((a) => lastObserved.has(a))) return;
|
|
574
|
+
await new Promise((r) => setTimeout(r, ASGProvider.TG_CONVERGENCE_POLL_INTERVAL_MS));
|
|
575
|
+
}
|
|
576
|
+
const expectedSorted = [...expected].sort();
|
|
577
|
+
const observedSorted = [...lastObserved].sort();
|
|
578
|
+
this.logger.warn(`applyTargetGroupArnsDiff: TG set did not converge within ${ASGProvider.TG_CONVERGENCE_TIMEOUT_MS}ms for ASG ${physicalId}. expected=${JSON.stringify(expectedSorted)} observed=${JSON.stringify(observedSorted)}`);
|
|
579
|
+
}
|
|
580
|
+
async applyMetricsCollectionDiff(physicalId, next, prev) {
|
|
581
|
+
if (JSON.stringify(next ?? []) === JSON.stringify(prev ?? [])) return;
|
|
582
|
+
const nextEntries = Array.isArray(next) ? next : [];
|
|
583
|
+
const prevEntries = Array.isArray(prev) ? prev : [];
|
|
584
|
+
const prevByGranularity = /* @__PURE__ */ new Map();
|
|
585
|
+
for (const e of prevEntries) if (e.Granularity) prevByGranularity.set(e.Granularity, e.Metrics);
|
|
586
|
+
const nextByGranularity = /* @__PURE__ */ new Map();
|
|
587
|
+
for (const e of nextEntries) if (e.Granularity) nextByGranularity.set(e.Granularity, e.Metrics);
|
|
588
|
+
for (const [granularity, metrics] of prevByGranularity) if (!nextByGranularity.has(granularity)) await this.getClient().send(new DisableMetricsCollectionCommand({
|
|
589
|
+
AutoScalingGroupName: physicalId,
|
|
590
|
+
...metrics && metrics.length > 0 ? { Metrics: metrics } : {}
|
|
591
|
+
}));
|
|
592
|
+
for (const [granularity, metrics] of nextByGranularity) {
|
|
593
|
+
const before = prevByGranularity.get(granularity);
|
|
594
|
+
if (JSON.stringify(before ?? null) === JSON.stringify(metrics ?? null)) continue;
|
|
595
|
+
if (before && before.length > 0) {
|
|
596
|
+
const removed = metrics ? before.filter((m) => !metrics.includes(m)) : [];
|
|
597
|
+
if (removed.length > 0) await this.getClient().send(new DisableMetricsCollectionCommand({
|
|
598
|
+
AutoScalingGroupName: physicalId,
|
|
599
|
+
Metrics: removed
|
|
600
|
+
}));
|
|
601
|
+
}
|
|
602
|
+
await this.getClient().send(new EnableMetricsCollectionCommand({
|
|
603
|
+
AutoScalingGroupName: physicalId,
|
|
604
|
+
Granularity: granularity,
|
|
605
|
+
...metrics && metrics.length > 0 ? { Metrics: metrics } : {}
|
|
606
|
+
}));
|
|
607
|
+
}
|
|
608
|
+
}
|
|
609
|
+
async applyLifecycleHooksDiff(physicalId, next, prev) {
|
|
610
|
+
if (JSON.stringify(next ?? []) === JSON.stringify(prev ?? [])) return;
|
|
611
|
+
const nextEntries = Array.isArray(next) ? next : [];
|
|
612
|
+
const prevEntries = Array.isArray(prev) ? prev : [];
|
|
613
|
+
const nextNames = new Set(nextEntries.map((e) => e.LifecycleHookName).filter((n) => !!n));
|
|
614
|
+
for (const e of prevEntries) if (e.LifecycleHookName && !nextNames.has(e.LifecycleHookName)) await this.getClient().send(new DeleteLifecycleHookCommand({
|
|
615
|
+
AutoScalingGroupName: physicalId,
|
|
616
|
+
LifecycleHookName: e.LifecycleHookName
|
|
617
|
+
}));
|
|
618
|
+
const prevByName = /* @__PURE__ */ new Map();
|
|
619
|
+
for (const e of prevEntries) if (e.LifecycleHookName) prevByName.set(e.LifecycleHookName, e);
|
|
620
|
+
for (const e of nextEntries) {
|
|
621
|
+
if (!e.LifecycleHookName) continue;
|
|
622
|
+
const prevHook = prevByName.get(e.LifecycleHookName);
|
|
623
|
+
if (JSON.stringify(prevHook) === JSON.stringify(e)) continue;
|
|
624
|
+
await this.getClient().send(new PutLifecycleHookCommand({
|
|
625
|
+
AutoScalingGroupName: physicalId,
|
|
626
|
+
LifecycleHookName: e.LifecycleHookName,
|
|
627
|
+
...e.LifecycleTransition !== void 0 && { LifecycleTransition: e.LifecycleTransition },
|
|
628
|
+
...e.RoleARN !== void 0 && { RoleARN: e.RoleARN },
|
|
629
|
+
...e.NotificationTargetARN !== void 0 && { NotificationTargetARN: e.NotificationTargetARN },
|
|
630
|
+
...e.NotificationMetadata !== void 0 && { NotificationMetadata: e.NotificationMetadata },
|
|
631
|
+
...e.HeartbeatTimeout !== void 0 && { HeartbeatTimeout: e.HeartbeatTimeout },
|
|
632
|
+
...e.DefaultResult !== void 0 && { DefaultResult: e.DefaultResult }
|
|
633
|
+
}));
|
|
634
|
+
}
|
|
635
|
+
}
|
|
636
|
+
async applyTrafficSourcesDiff(physicalId, next, prev) {
|
|
637
|
+
if (JSON.stringify(next ?? []) === JSON.stringify(prev ?? [])) return;
|
|
638
|
+
const nextEntries = Array.isArray(next) ? next : [];
|
|
639
|
+
const prevEntries = Array.isArray(prev) ? prev : [];
|
|
640
|
+
const nextIds = new Set(nextEntries.map((e) => e.Identifier).filter((i) => !!i));
|
|
641
|
+
const prevIds = new Set(prevEntries.map((e) => e.Identifier).filter((i) => !!i));
|
|
642
|
+
const toDetach = prevEntries.filter((e) => e.Identifier && !nextIds.has(e.Identifier));
|
|
643
|
+
const toAttach = nextEntries.filter((e) => e.Identifier && !prevIds.has(e.Identifier));
|
|
644
|
+
if (toDetach.length > 0) await this.getClient().send(new DetachTrafficSourcesCommand({
|
|
645
|
+
AutoScalingGroupName: physicalId,
|
|
646
|
+
TrafficSources: toDetach.map((e) => ({
|
|
647
|
+
Identifier: e.Identifier,
|
|
648
|
+
...e.Type !== void 0 && { Type: e.Type }
|
|
649
|
+
}))
|
|
650
|
+
}));
|
|
651
|
+
if (toAttach.length > 0) await this.getClient().send(new AttachTrafficSourcesCommand({
|
|
652
|
+
AutoScalingGroupName: physicalId,
|
|
653
|
+
TrafficSources: toAttach.map((e) => ({
|
|
654
|
+
Identifier: e.Identifier,
|
|
655
|
+
...e.Type !== void 0 && { Type: e.Type }
|
|
656
|
+
}))
|
|
657
|
+
}));
|
|
658
|
+
}
|
|
659
|
+
async applyNotificationConfigurationsDiff(physicalId, next, prev) {
|
|
660
|
+
if (JSON.stringify(next ?? []) === JSON.stringify(prev ?? [])) return;
|
|
661
|
+
const nextEntries = Array.isArray(next) ? next : [];
|
|
662
|
+
const prevEntries = Array.isArray(prev) ? prev : [];
|
|
663
|
+
const nextByTopic = /* @__PURE__ */ new Map();
|
|
664
|
+
for (const e of nextEntries) if (e.TopicARN) nextByTopic.set(e.TopicARN, e.NotificationTypes);
|
|
665
|
+
const prevByTopic = /* @__PURE__ */ new Map();
|
|
666
|
+
for (const e of prevEntries) if (e.TopicARN) prevByTopic.set(e.TopicARN, e.NotificationTypes);
|
|
667
|
+
for (const topic of prevByTopic.keys()) if (!nextByTopic.has(topic)) await this.getClient().send(new DeleteNotificationConfigurationCommand({
|
|
668
|
+
AutoScalingGroupName: physicalId,
|
|
669
|
+
TopicARN: topic
|
|
670
|
+
}));
|
|
671
|
+
for (const [topic, types] of nextByTopic) {
|
|
672
|
+
const before = prevByTopic.get(topic);
|
|
673
|
+
if (JSON.stringify(before ?? null) === JSON.stringify(types ?? null)) continue;
|
|
674
|
+
await this.getClient().send(new PutNotificationConfigurationCommand({
|
|
675
|
+
AutoScalingGroupName: physicalId,
|
|
676
|
+
TopicARN: topic,
|
|
677
|
+
NotificationTypes: types ?? []
|
|
678
|
+
}));
|
|
679
|
+
}
|
|
680
|
+
}
|
|
681
|
+
};
|
|
682
|
+
/**
|
|
683
|
+
* Reverse-map AWS `EnabledMetrics: [{Metric, Granularity}]` (flat list,
|
|
684
|
+
* one row per enabled metric) back to the CFn array shape
|
|
685
|
+
* `[{Granularity, Metrics?[]}]`. Metrics with the same Granularity are
|
|
686
|
+
* grouped together; the resulting Metrics list is sorted alphabetically
|
|
687
|
+
* for stable positional compare in the drift comparator.
|
|
688
|
+
*
|
|
689
|
+
* Always returns a placeholder `[]` per the cdkd PR #145 always-emit
|
|
690
|
+
* convention so a console-side EnableMetricsCollection on a previously-
|
|
691
|
+
* empty group surfaces as drift on the v3 `observedProperties` baseline.
|
|
692
|
+
*/
|
|
693
|
+
function mapEnabledMetricsToCfn(enabledMetrics) {
|
|
694
|
+
if (!enabledMetrics || enabledMetrics.length === 0) return [];
|
|
695
|
+
const byGranularity = /* @__PURE__ */ new Map();
|
|
696
|
+
for (const e of enabledMetrics) {
|
|
697
|
+
const g = e.Granularity;
|
|
698
|
+
if (!g) continue;
|
|
699
|
+
let set = byGranularity.get(g);
|
|
700
|
+
if (!set) {
|
|
701
|
+
set = /* @__PURE__ */ new Set();
|
|
702
|
+
byGranularity.set(g, set);
|
|
703
|
+
}
|
|
704
|
+
if (e.Metric) set.add(e.Metric);
|
|
705
|
+
}
|
|
706
|
+
const result = [];
|
|
707
|
+
for (const granularity of Array.from(byGranularity.keys()).sort()) {
|
|
708
|
+
const metrics = Array.from(byGranularity.get(granularity) ?? []).sort();
|
|
709
|
+
result.push(metrics.length > 0 ? {
|
|
710
|
+
Granularity: granularity,
|
|
711
|
+
Metrics: metrics
|
|
712
|
+
} : { Granularity: granularity });
|
|
713
|
+
}
|
|
714
|
+
return result;
|
|
715
|
+
}
|
|
716
|
+
/**
|
|
717
|
+
* Reverse-map AWS `DescribeLifecycleHooks` response to the CFn
|
|
718
|
+
* `LifecycleHookSpecificationList` shape. Each hook is surfaced under the
|
|
719
|
+
* exact CFn property name. AWS-side fields cdkd state never carried
|
|
720
|
+
* (`AutoScalingGroupName` — duplicated on every hook by AWS,
|
|
721
|
+
* `GlobalTimeout` — AWS-derived) are filtered out. Sorted by
|
|
722
|
+
* LifecycleHookName for stable positional compare.
|
|
723
|
+
*/
|
|
724
|
+
function mapLifecycleHooksToCfn(hooks) {
|
|
725
|
+
if (!hooks || hooks.length === 0) return [];
|
|
726
|
+
const result = [];
|
|
727
|
+
for (const h of hooks) {
|
|
728
|
+
if (!h.LifecycleHookName) continue;
|
|
729
|
+
const entry = { LifecycleHookName: h.LifecycleHookName };
|
|
730
|
+
if (h.LifecycleTransition !== void 0) entry["LifecycleTransition"] = h.LifecycleTransition;
|
|
731
|
+
if (h.RoleARN !== void 0) entry["RoleARN"] = h.RoleARN;
|
|
732
|
+
if (h.NotificationTargetARN !== void 0) entry["NotificationTargetARN"] = h.NotificationTargetARN;
|
|
733
|
+
if (h.NotificationMetadata !== void 0) entry["NotificationMetadata"] = h.NotificationMetadata;
|
|
734
|
+
if (h.HeartbeatTimeout !== void 0) entry["HeartbeatTimeout"] = h.HeartbeatTimeout;
|
|
735
|
+
if (h.DefaultResult !== void 0) entry["DefaultResult"] = h.DefaultResult;
|
|
736
|
+
result.push(entry);
|
|
737
|
+
}
|
|
738
|
+
result.sort((a, b) => String(a["LifecycleHookName"]).localeCompare(String(b["LifecycleHookName"])));
|
|
739
|
+
return result;
|
|
740
|
+
}
|
|
741
|
+
/**
|
|
742
|
+
* Reverse-map AWS `DescribeTrafficSources` response to the CFn
|
|
743
|
+
* `TrafficSources` shape `[{Identifier, Type?}]`. AWS-side runtime fields
|
|
744
|
+
* (`State`, the deprecated `TrafficSource` alias) are filtered out.
|
|
745
|
+
* Sorted by Identifier for stable positional compare.
|
|
746
|
+
*/
|
|
747
|
+
function mapTrafficSourcesToCfn(trafficSources) {
|
|
748
|
+
if (!trafficSources || trafficSources.length === 0) return [];
|
|
749
|
+
const result = [];
|
|
750
|
+
for (const t of trafficSources) {
|
|
751
|
+
if (!t.Identifier) continue;
|
|
752
|
+
const entry = { Identifier: t.Identifier };
|
|
753
|
+
if (t.Type !== void 0) entry["Type"] = t.Type;
|
|
754
|
+
result.push(entry);
|
|
755
|
+
}
|
|
756
|
+
result.sort((a, b) => String(a["Identifier"]).localeCompare(String(b["Identifier"])));
|
|
757
|
+
return result;
|
|
758
|
+
}
|
|
759
|
+
/**
|
|
760
|
+
* Reverse-map AWS `DescribeNotificationConfigurations` (a flat list, one
|
|
761
|
+
* row per `(topicArn, notificationType)`) into the CFn shape
|
|
762
|
+
* `[{TopicARN, NotificationTypes[]}]`. NotificationTypes are grouped per
|
|
763
|
+
* TopicARN and sorted alphabetically for stable positional compare.
|
|
764
|
+
*/
|
|
765
|
+
function mapNotificationsToCfn(configurations) {
|
|
766
|
+
if (!configurations || configurations.length === 0) return [];
|
|
767
|
+
const byTopic = /* @__PURE__ */ new Map();
|
|
768
|
+
for (const c of configurations) {
|
|
769
|
+
if (!c.TopicARN) continue;
|
|
770
|
+
let set = byTopic.get(c.TopicARN);
|
|
771
|
+
if (!set) {
|
|
772
|
+
set = /* @__PURE__ */ new Set();
|
|
773
|
+
byTopic.set(c.TopicARN, set);
|
|
774
|
+
}
|
|
775
|
+
if (c.NotificationType) set.add(c.NotificationType);
|
|
776
|
+
}
|
|
777
|
+
const result = [];
|
|
778
|
+
for (const topic of Array.from(byTopic.keys()).sort()) {
|
|
779
|
+
const types = Array.from(byTopic.get(topic) ?? []).sort();
|
|
780
|
+
result.push({
|
|
781
|
+
TopicARN: topic,
|
|
782
|
+
NotificationTypes: types
|
|
783
|
+
});
|
|
784
|
+
}
|
|
785
|
+
return result;
|
|
786
|
+
}
|
|
787
|
+
|
|
788
|
+
//#endregion
|
|
789
|
+
export { asg_provider_exports as n, ASGProvider as t };
|
|
790
|
+
//# sourceMappingURL=asg-provider-B_hrCxRx.js.map
|