1import * as amp from "@distilled.cloud/aws/amp";
2import * as Effect from "effect/Effect";
3import * as Schedule from "effect/Schedule";
4import { isResolved } from "../../Diff.ts";
5import * as Provider from "../../Provider.ts";
6import { Resource } from "../../Resource.ts";
7import type { Providers } from "../Providers.ts";
8import { decodeDefinition, encodeDefinition } from "./internal.ts";
9
10export interface AlertManagerDefinitionProps {
11 /**
12 * Id of the AMP workspace this alert manager definition belongs to. A
13 * workspace has at most one alert manager definition. Changing the
14 * workspace replaces the definition.
15 */
16 workspaceId: string;
17 /**
18 * The Alertmanager configuration as a YAML document (the `alertmanager.yml`
19 * shape, with `alertmanager_config` and optional `template_files` keys).
20 * Updated in place.
21 */
22 definition: string;
23}
24
25export interface AlertManagerDefinition extends Resource<
26 "AWS.AMP.AlertManagerDefinition",
27 AlertManagerDefinitionProps,
28 {
29 workspaceId: string;
30 status: string;
31 },
32 never,
33 Providers
34> {}
35
36/**
37 * The Alertmanager definition for an Amazon Managed Service for Prometheus
38 * workspace — configures how firing alerts are grouped, routed, and
39 * dispatched to receivers (SNS, etc.). A workspace has at most one.
40 *
41 * ### Creating an Alert Manager Definition
42 * **Example:** Basic Definition
43 * ```typescript
44 * const workspace = yield* AMP.Workspace("Metrics", {});
45 * const alerts = yield* AMP.AlertManagerDefinition("Alerts", {
46 * workspaceId: workspace.workspaceId,
47 * definition: `alertmanager_config: |
48 * route:
49 * receiver: default
50 * receivers:
51 * - name: default`,
52 * });
53 * ```
54 *
55 * @resource
56 */
57export const AlertManagerDefinition = Resource<AlertManagerDefinition>(
58 "AWS.AMP.AlertManagerDefinition",
59);
60
61export const AlertManagerDefinitionProvider = () =>
62 Provider.effect(
63 AlertManagerDefinition,
64 Effect.gen(function* () {
65 /** Describe the definition; typed not-found → undefined. */
66 const describe = Effect.fn(function* (workspaceId: string) {
67 const response = yield* amp
68 .describeAlertManagerDefinition({ workspaceId })
69 .pipe(
70 Effect.catchTag("ResourceNotFoundException", () =>
71 Effect.succeed(undefined),
72 ),
73 );
74 return response?.alertManagerDefinition;
75 });
76
77 /**
78 * Best-effort poll toward ACTIVE. The workspace's Alertmanager
79 * component can take a few minutes to provision, so we do not block a
80 * deploy on it: we wait a bounded window and return the last observed
81 * status (CREATING converges on a later reconcile). A definition in
82 * CREATING / UPDATING omits its `data` blob from the description.
83 */
84 const waitActive = Effect.fn(function* (workspaceId: string) {
85 return yield* amp.describeAlertManagerDefinition({ workspaceId }).pipe(
86 Effect.map((r) => r.alertManagerDefinition),
87 Effect.repeat({
88 schedule: Schedule.max([
89 Schedule.fixed("3 seconds"),
90 Schedule.recurs(20),
91 ]),
92 until: (d) => d.status.statusCode === "ACTIVE",
93 }),
94 );
95 });
96
97 return {
98 stables: ["workspaceId"],
99
100 diff: Effect.fn(function* ({ olds, news }) {
101 if (!isResolved(news)) return undefined;
102 if (olds?.workspaceId !== news.workspaceId) {
103 return { action: "replace" } as const;
104 }
105 }),
106
107 read: Effect.fn(function* ({ olds, output }) {
108 const workspaceId = output?.workspaceId ?? olds?.workspaceId;
109 if (!workspaceId) return undefined;
110 const def = yield* describe(workspaceId);
111 if (def === undefined) return undefined;
112 // AMP alert manager definitions are not taggable — ownership is
113 // implied by the owned parent workspace.
114 return { workspaceId, status: def.status.statusCode };
115 }),
116
117 reconcile: Effect.fn(function* ({ news, session }) {
118 const workspaceId = news!.workspaceId;
119 const desiredData = yield* encodeDefinition(news!.definition);
120
121 // 1. Observe — the live definition is authoritative.
122 const existing = yield* describe(workspaceId);
123
124 // 2/3. Ensure + sync — create when absent, put when the YAML drifts.
125 // `data` is present once ACTIVE (omitted while CREATING).
126 if (existing === undefined) {
127 yield* amp.createAlertManagerDefinition({
128 workspaceId,
129 data: desiredData,
130 });
131 } else {
132 const currentDefinition =
133 existing.data !== undefined
134 ? yield* decodeDefinition(existing.data)
135 : undefined;
136 if (currentDefinition !== news!.definition) {
137 yield* amp.putAlertManagerDefinition({
138 workspaceId,
139 data: desiredData,
140 });
141 }
142 }
143
144 // Wait for ACTIVE so the returned status and data blob are settled.
145 const fresh = yield* waitActive(workspaceId);
146
147 yield* session.note(workspaceId);
148 return { workspaceId, status: fresh.status.statusCode };
149 }),
150
151 delete: Effect.fn(function* ({ output }) {
152 yield* amp
153 .deleteAlertManagerDefinition({ workspaceId: output.workspaceId })
154 .pipe(
155 Effect.catchTag("ResourceNotFoundException", () => Effect.void),
156 Effect.retry({
157 while: (e) => e._tag === "ConflictException",
158 schedule: Schedule.max([
159 Schedule.fixed("3 seconds"),
160 Schedule.recurs(20),
161 ]),
162 }),
163 );
164 }),
165
166 // Singleton sub-resource keyed by its parent workspace.
167 list: () => Effect.succeed([]),
168 };
169 }),
170 );