1import * as cloudwatch from "@distilled.cloud/aws/cloudwatch";
2import * as Effect from "effect/Effect";
3import * as Stream from "effect/Stream";
4import { isResolved } from "../../Diff.ts";
5import * as Provider from "../../Provider.ts";
6import { Resource } from "../../Resource.ts";
7import { AWSEnvironment, type AccountID } from "../Environment.ts";
8import type { Providers } from "../Providers.ts";
9import type { RegionID } from "../Region.ts";
10import { createManagedTags, createName, retryConcurrent } from "./common.ts";
11
12export type AlarmMuteRuleName = string;
13export type AlarmMuteRuleArn =
14 `arn:aws:cloudwatch:${RegionID}:${AccountID}:alarm-mute-rule:${string}`;
15
16export interface AlarmMuteRuleProps extends Omit<
17 cloudwatch.PutAlarmMuteRuleInput,
18 "Name" | "Tags"
19> {
20 /**
21 * Name of the mute rule. If omitted, a unique name is generated.
22 */
23 name?: AlarmMuteRuleName;
24 /**
25 * Optional tags to apply to the mute rule.
26 */
27 tags?: Record<string, string>;
28}
29
30export interface AlarmMuteRule extends Resource<
31 "AWS.CloudWatch.AlarmMuteRule",
32 AlarmMuteRuleProps,
33 {
34 /** Physical name of the mute rule. */
35 alarmMuteRuleName: AlarmMuteRuleName;
36 /** ARN of the mute rule. */
37 alarmMuteRuleArn: AlarmMuteRuleArn;
38 /** Current status of the mute rule. */
39 status: string | undefined;
40 /** The mute type of the rule. */
41 muteType: string | undefined;
42 /** The full mute rule description as last read from CloudWatch. */
43 alarmMuteRule: cloudwatch.GetAlarmMuteRuleOutput;
44 /** Tags on the mute rule, including the internal Alchemy ownership tags. */
45 tags: Record<string, string>;
46 },
47 never,
48 Providers
49> {}
50
51/**
52 * A CloudWatch alarm mute rule — suppresses alarm actions on a recurring
53 * schedule (e.g. maintenance windows) instead of manually disabling and
54 * re-enabling alarm actions.
55 * ### Creating Mute Rules
56 * **Example:** Scheduled Mute
57 * ```typescript
58 * const rule = yield* AlarmMuteRule("NightlyMute", {
59 * Rule: {
60 * Schedule: {
61 * Expression: "0 2 * * SUN",
62 * Duration: "PT1H",
63 * },
64 * },
65 * });
66 * ```
67 *
68 * ### Reading Mute Rules at Runtime
69 * **Example:** Read the Mute Rule from a Function
70 * ```typescript
71 * // init — bind the rule to the function (see GetAlarmMuteRule)
72 * const getAlarmMuteRule = yield* AWS.CloudWatch.GetAlarmMuteRule(rule);
73 *
74 * // runtime
75 * const result = yield* getAlarmMuteRule();
76 * const schedule = result.Rule?.Schedule;
77 * ```
78 *
79 * @resource
80 */
81export const AlarmMuteRule = Resource<AlarmMuteRule>(
82 "AWS.CloudWatch.AlarmMuteRule",
83);
84
85export const AlarmMuteRuleProvider = () =>
86 Provider.effect(
87 AlarmMuteRule,
88 Effect.gen(function* () {
89 const createMuteRuleName = (id: string, props: { name?: string } = {}) =>
90 createName(id, props.name, 255);
91
92 const alarmMuteRuleArn = (name: string) =>
93 AWSEnvironment.current.pipe(
94 Effect.map(
95 (env) =>
96 `arn:aws:cloudwatch:${env.region}:${env.accountId}:alarm-mute-rule:${name}` as AlarmMuteRuleArn,
97 ),
98 );
99
100 const readAlarmMuteRule = Effect.fn(function* (name: string) {
101 const output = yield* cloudwatch
102 .getAlarmMuteRule({
103 AlarmMuteRuleName: name,
104 })
105 .pipe(
106 Effect.catchTag("ResourceNotFoundException", () =>
107 Effect.succeed(undefined),
108 ),
109 );
110
111 if (!output?.Name || !output.AlarmMuteRuleArn) {
112 return undefined;
113 }
114
115 return {
116 alarmMuteRuleName: output.Name,
117 alarmMuteRuleArn: output.AlarmMuteRuleArn as AlarmMuteRuleArn,
118 status: output.Status,
119 muteType: output.MuteType,
120 alarmMuteRule: output,
121 tags: {},
122 };
123 });
124
125 return {
126 stables: ["alarmMuteRuleName", "alarmMuteRuleArn"],
127 diff: Effect.fn(function* ({ id, olds = {}, news = {} }) {
128 if (!isResolved(news)) return undefined;
129 const oldName = yield* createMuteRuleName(id, olds);
130 const newName = yield* createMuteRuleName(id, news);
131
132 if (oldName !== newName) {
133 return { action: "replace" } as const;
134 }
135 }),
136 read: Effect.fn(function* ({ id, olds, output }) {
137 const name =
138 output?.alarmMuteRuleName ??
139 (yield* createMuteRuleName(id, olds ?? {}));
140 return yield* readAlarmMuteRule(name);
141 }),
142 // AWS account/region collection: `listAlarmMuteRules` paginates every
143 // mute rule in the region. The summary only carries the ARN, so we
144 // derive the name from it and re-read each rule to return the full
145 // `Attributes` shape (identical to `read`).
146 list: () =>
147 Effect.gen(function* () {
148 const summaries = yield* cloudwatch.listAlarmMuteRules
149 .pages({})
150 .pipe(
151 Stream.runCollect,
152 Effect.map((chunk) =>
153 Array.from(chunk).flatMap(
154 (page) => page.AlarmMuteRuleSummaries ?? [],
155 ),
156 ),
157 );
158
159 const rows = yield* Effect.forEach(
160 summaries,
161 (summary) => {
162 const name =
163 summary.AlarmMuteRuleArn?.split(":alarm-mute-rule:")[1];
164 if (!name) {
165 return Effect.succeed(undefined);
166 }
167 return readAlarmMuteRule(name);
168 },
169 { concurrency: 10 },
170 );
171
172 return rows.filter(
173 (row): row is NonNullable<typeof row> => row !== undefined,
174 );
175 }),
176 reconcile: Effect.fn(function* ({ id, news, output, session }) {
177 // Observe — pin the physical name from `output` if we already
178 // have one; otherwise derive it from desired props.
179 const name =
180 output?.alarmMuteRuleName ?? (yield* createMuteRuleName(id, news));
181
182 // Ensure — `putAlarmMuteRule` is an upsert. The CloudWatch API
183 // accepts `Tags` on every put, so we send the full managed tag
184 // set every reconcile and let the API converge it.
185 const tags = yield* createManagedTags(id, news.tags);
186
187 yield* retryConcurrent(
188 cloudwatch.putAlarmMuteRule({
189 ...news,
190 Name: name,
191 Tags: Object.entries(tags).map(([Key, Value]) => ({
192 Key,
193 Value,
194 })),
195 }),
196 );
197
198 yield* session.note(yield* alarmMuteRuleArn(name));
199
200 const state = yield* readAlarmMuteRule(name);
201 if (!state) {
202 return yield* Effect.fail(
203 new Error(`failed to read reconciled alarm mute rule '${name}'`),
204 );
205 }
206
207 return {
208 ...state,
209 tags,
210 };
211 }),
212 delete: Effect.fn(function* ({ output }) {
213 yield* retryConcurrent(
214 cloudwatch.deleteAlarmMuteRule({
215 AlarmMuteRuleName: output.alarmMuteRuleName,
216 }),
217 );
218 }),
219 };
220 }),
221 );