1import * as devopsguru from "@distilled.cloud/aws/devops-guru";
2import * as Effect from "effect/Effect";
3import * as Schedule from "effect/Schedule";
4import { Unowned } from "../../AdoptPolicy.ts";
5import * as Provider from "../../Provider.ts";
6import { Resource } from "../../Resource.ts";
7import type { Providers } from "../Providers.ts";
8
9export interface ServiceIntegrationProps {
10 /**
11 * Create an AWS Systems Manager OpsItem for each new insight so incidents
12 * flow into OpsCenter.
13 * @default false
14 */
15 opsCenter?: boolean;
16 /**
17 * Analyze CloudWatch log groups in the resource collection for log
18 * anomalies and surface them on insights.
19 * @default false
20 */
21 logsAnomalyDetection?: boolean;
22 /**
23 * Encrypt DevOps Guru data with this customer-managed KMS key instead of
24 * the AWS-owned default key. Omit to use the AWS-owned key.
25 */
26 kmsKeyId?: string;
27}
28
29export interface ServiceIntegration extends Resource<
30 "AWS.DevOpsGuru.ServiceIntegration",
31 ServiceIntegrationProps,
32 {
33 /** Whether an OpsItem is created for each new insight. */
34 opsCenter: boolean;
35 /** Whether CloudWatch log groups are analyzed for anomalies. */
36 logsAnomalyDetection: boolean;
37 /** Server-side encryption key type (`AWS_OWNED_KMS_KEY` or `CUSTOMER_MANAGED_KEY`). */
38 encryptionType: devopsguru.ServerSideEncryptionType;
39 /** Customer-managed KMS key id, when `encryptionType` is `CUSTOMER_MANAGED_KEY`. */
40 kmsKeyId: string | undefined;
41 },
42 never,
43 Providers
44> {}
45
46/**
47 * The DevOps Guru service integration — the account/region singleton that
48 * controls how DevOps Guru integrates with other AWS services: creating a
49 * Systems Manager OpsItem for each insight, analyzing CloudWatch log groups
50 * for anomalies, and encrypting DevOps Guru data with a customer-managed
51 * KMS key.
52 *
53 * An account has exactly one integration configuration, so this resource is
54 * a capture-and-restore singleton: adopting a non-default configuration that
55 * Alchemy did not create requires `--adopt`. Destroying the resource
56 * restores the account defaults (everything disabled, AWS-owned key).
57 *
58 * ### Configuring the Integration
59 * **Example:** Enable Log Anomaly Detection
60 * ```typescript
61 * const integration = yield* DevOpsGuru.ServiceIntegration("Integration", {
62 * logsAnomalyDetection: true,
63 * });
64 * ```
65 *
66 * **Example:** File an OpsItem for Every Insight
67 * ```typescript
68 * const integration = yield* DevOpsGuru.ServiceIntegration("Integration", {
69 * opsCenter: true,
70 * logsAnomalyDetection: true,
71 * });
72 * ```
73 *
74 * **Example:** Encrypt with a Customer-Managed Key
75 * ```typescript
76 * const integration = yield* DevOpsGuru.ServiceIntegration("Integration", {
77 * kmsKeyId: key.keyId,
78 * });
79 * ```
80 *
81 * @resource
82 */
83export const ServiceIntegration = Resource<ServiceIntegration>(
84 "AWS.DevOpsGuru.ServiceIntegration",
85);
86
87interface ObservedIntegration {
88 opsCenter: boolean;
89 logsAnomalyDetection: boolean;
90 encryptionType: devopsguru.ServerSideEncryptionType;
91 kmsKeyId: string | undefined;
92}
93
94const isDefault = (observed: ObservedIntegration) =>
95 !observed.opsCenter &&
96 !observed.logsAnomalyDetection &&
97 observed.encryptionType === "AWS_OWNED_KMS_KEY";
98
99/**
100 * Concurrent `UpdateServiceIntegration` calls conflict server-side — retry
101 * the typed conflict tag on a short bounded schedule.
102 *
103 * Explicitly typed: inlining `Effect.retry` with options in provider
104 * lifecycle code can widen the provider layer to `unknown` in declaration
105 * emit.
106 *
107 * @internal
108 */
109const retryUpdateConflict = <A, E extends { readonly _tag: string }, R>(
110 self: Effect.Effect<A, E, R>,
111): Effect.Effect<A, E, R> =>
112 Effect.retry(self, {
113 while: (e) => e._tag === "ConflictException",
114 schedule: Schedule.max([Schedule.fixed("2 seconds"), Schedule.recurs(8)]),
115 });
116
117export const ServiceIntegrationProvider = () =>
118 Provider.effect(
119 ServiceIntegration,
120 Effect.gen(function* () {
121 // Observe the live account configuration. Absent sections and absent
122 // opt-in statuses mean "account default" (disabled / AWS-owned key).
123 const observe = Effect.gen(function* () {
124 const { ServiceIntegration: config } =
125 yield* devopsguru.describeServiceIntegration({});
126 const kms = config?.KMSServerSideEncryption;
127 const encryptionType: devopsguru.ServerSideEncryptionType =
128 kms?.Type ?? "AWS_OWNED_KMS_KEY";
129 return {
130 opsCenter: config?.OpsCenter?.OptInStatus === "ENABLED",
131 logsAnomalyDetection:
132 config?.LogsAnomalyDetection?.OptInStatus === "ENABLED",
133 encryptionType,
134 kmsKeyId:
135 encryptionType === "CUSTOMER_MANAGED_KEY"
136 ? kms?.KMSKeyId
137 : undefined,
138 } satisfies ObservedIntegration;
139 });
140
141 const update = Effect.fn(function* (
142 config: devopsguru.UpdateServiceIntegrationConfig,
143 ) {
144 yield* retryUpdateConflict(
145 devopsguru.updateServiceIntegration({ ServiceIntegration: config }),
146 );
147 });
148
149 // Converge the live configuration to the desired shape, one section
150 // per update call so a partial failure leaves the others converged.
151 const converge = Effect.fn(function* (
152 observed: ObservedIntegration,
153 desired: ServiceIntegrationProps,
154 ) {
155 const wantOpsCenter = desired.opsCenter ?? false;
156 if (observed.opsCenter !== wantOpsCenter) {
157 yield* update({
158 OpsCenter: {
159 OptInStatus: wantOpsCenter ? "ENABLED" : "DISABLED",
160 },
161 });
162 }
163
164 const wantLogs = desired.logsAnomalyDetection ?? false;
165 if (observed.logsAnomalyDetection !== wantLogs) {
166 yield* update({
167 LogsAnomalyDetection: {
168 OptInStatus: wantLogs ? "ENABLED" : "DISABLED",
169 },
170 });
171 }
172
173 if (desired.kmsKeyId !== undefined) {
174 if (
175 observed.encryptionType !== "CUSTOMER_MANAGED_KEY" ||
176 observed.kmsKeyId !== desired.kmsKeyId
177 ) {
178 yield* update({
179 KMSServerSideEncryption: {
180 Type: "CUSTOMER_MANAGED_KEY",
181 KMSKeyId: desired.kmsKeyId,
182 OptInStatus: "ENABLED",
183 },
184 });
185 }
186 } else if (observed.encryptionType === "CUSTOMER_MANAGED_KEY") {
187 yield* update({
188 KMSServerSideEncryption: {
189 Type: "AWS_OWNED_KMS_KEY",
190 OptInStatus: "ENABLED",
191 },
192 });
193 }
194 });
195
196 return {
197 // Account/region singleton — surfaced only when non-default.
198 list: () =>
199 observe.pipe(
200 Effect.map((observed) => (isDefault(observed) ? [] : [observed])),
201 ),
202
203 read: Effect.fn(function* ({ output }) {
204 const observed = yield* observe;
205 if (isDefault(observed)) {
206 return undefined;
207 }
208 // The integration can't carry ownership tags — a non-default
209 // configuration we have no record of belongs to someone else
210 // until explicitly adopted.
211 return output !== undefined ? observed : Unowned(observed);
212 }),
213
214 reconcile: Effect.fn(function* ({ news, session }) {
215 // 1. OBSERVE — the live account configuration is authoritative.
216 const observed = yield* observe;
217
218 // 2. SYNC — apply only the changed sections.
219 yield* converge(observed, news);
220
221 // 3. RETURN fresh attributes.
222 const final = yield* observe;
223 yield* session.note(
224 `opsCenter: ${final.opsCenter}, logsAnomalyDetection: ${final.logsAnomalyDetection}, encryption: ${final.encryptionType}`,
225 );
226 return final;
227 }),
228
229 // "Deleting" the singleton restores the account defaults.
230 delete: Effect.fn(function* () {
231 const observed = yield* observe;
232 yield* converge(observed, {});
233 }),
234 };
235 }),
236 );