@go-to-k/cdkd 0.219.1 → 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.
@@ -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