1import * as rds from "@distilled.cloud/aws/rds";
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 type { Providers } from "../Providers.ts";
8
9export interface DBProxyTargetGroupProps {
10 /**
11 * Proxy that owns the target group.
12 */
13 dbProxyName: string;
14 /**
15 * Target group name.
16 * @default "default"
17 */
18 targetGroupName?: string;
19 /**
20 * Cluster targets registered with the proxy.
21 */
22 dbClusterIdentifiers?: string[];
23 /**
24 * Instance targets registered with the proxy.
25 */
26 dbInstanceIdentifiers?: string[];
27 /**
28 * Connection pool configuration.
29 */
30 connectionPoolConfig?: rds.ConnectionPoolConfiguration;
31}
32
33export interface DBProxyTargetGroup extends Resource<
34 "AWS.RDS.DBProxyTargetGroup",
35 DBProxyTargetGroupProps,
36 {
37 /**
38 * Proxy that owns the target group.
39 */
40 dbProxyName: string;
41 /**
42 * Name of the target group (`default` unless overridden).
43 */
44 targetGroupName: string;
45 /**
46 * ARN of the target group.
47 */
48 targetGroupArn: string | undefined;
49 /**
50 * Status of the target group (e.g. `available`).
51 */
52 status: string | undefined;
53 /**
54 * Whether this is the proxy's default target group.
55 */
56 isDefault: boolean | undefined;
57 /**
58 * Observed connection pool configuration.
59 */
60 connectionPoolConfig: rds.ConnectionPoolConfigurationInfo | undefined;
61 /**
62 * Cluster targets registered with the proxy.
63 */
64 dbClusterIdentifiers: string[];
65 /**
66 * Instance targets registered with the proxy.
67 */
68 dbInstanceIdentifiers: string[];
69 },
70 never,
71 Providers
72> {}
73
74/**
75 * The proxy target group that registers Aurora clusters or instances behind an
76 * RDS Proxy.
77 *
78 * Every `DBProxy` has exactly one `default` target group; this resource
79 * adopts it, tunes its connection pool, and reconciles the registered
80 * cluster/instance targets. Deleting it deregisters the targets rather than
81 * deleting the group itself.
82 * ### Registering Targets
83 * **Example:** Register a Cluster Behind a Proxy
84 * ```typescript
85 * const targets = yield* DBProxyTargetGroup("ProxyTargets", {
86 * dbProxyName: proxy.dbProxyName,
87 * dbClusterIdentifiers: [cluster.dbClusterIdentifier],
88 * });
89 * ```
90 *
91 * **Example:** Tune the Connection Pool
92 * ```typescript
93 * const targets = yield* DBProxyTargetGroup("ProxyTargets", {
94 * dbProxyName: proxy.dbProxyName,
95 * dbClusterIdentifiers: [cluster.dbClusterIdentifier],
96 * connectionPoolConfig: {
97 * MaxConnectionsPercent: 90,
98 * MaxIdleConnectionsPercent: 10,
99 * },
100 * });
101 * ```
102 *
103 * @resource
104 */
105export const DBProxyTargetGroup = Resource<DBProxyTargetGroup>(
106 "AWS.RDS.DBProxyTargetGroup",
107);
108
109const toTargetGroupName = (props: DBProxyTargetGroupProps) =>
110 props.targetGroupName ?? "default";
111
112export const DBProxyTargetGroupProvider = () =>
113 Provider.effect(
114 DBProxyTargetGroup,
115 Effect.gen(function* () {
116 const readGroup = Effect.fn(function* ({
117 dbProxyName,
118 targetGroupName,
119 }: {
120 dbProxyName: string;
121 targetGroupName: string;
122 }) {
123 const response = yield* rds
124 .describeDBProxyTargetGroups({
125 DBProxyName: dbProxyName,
126 TargetGroupName: targetGroupName,
127 })
128 .pipe(
129 Effect.catchTag("DBProxyTargetGroupNotFoundFault", () =>
130 Effect.succeed(undefined),
131 ),
132 );
133 return response?.TargetGroups?.[0];
134 });
135
136 const toAttrs = ({
137 group,
138 props,
139 }: {
140 group: rds.DBProxyTargetGroup;
141 props: DBProxyTargetGroupProps;
142 }): DBProxyTargetGroup["Attributes"] => ({
143 dbProxyName: group.DBProxyName ?? props.dbProxyName,
144 targetGroupName: group.TargetGroupName ?? toTargetGroupName(props),
145 targetGroupArn: group.TargetGroupArn,
146 status: group.Status,
147 isDefault: group.IsDefault,
148 connectionPoolConfig: group.ConnectionPoolConfig,
149 dbClusterIdentifiers: props.dbClusterIdentifiers ?? [],
150 dbInstanceIdentifiers: props.dbInstanceIdentifiers ?? [],
151 });
152
153 const readTargets = Effect.fn(function* ({
154 dbProxyName,
155 targetGroupName,
156 }: {
157 dbProxyName: string;
158 targetGroupName: string;
159 }) {
160 const response = yield* rds
161 .describeDBProxyTargets({
162 DBProxyName: dbProxyName,
163 TargetGroupName: targetGroupName,
164 })
165 .pipe(
166 Effect.catchTag("DBProxyTargetNotFoundFault", () =>
167 Effect.succeed(undefined),
168 ),
169 Effect.catchTag("DBProxyTargetGroupNotFoundFault", () =>
170 Effect.succeed(undefined),
171 ),
172 Effect.catchTag("DBProxyNotFoundFault", () =>
173 Effect.succeed(undefined),
174 ),
175 );
176 const targets = response?.Targets ?? [];
177 const observedClusters: string[] = [];
178 const observedInstances: string[] = [];
179 for (const target of targets) {
180 const id = target.RdsResourceId;
181 if (!id) continue;
182 if (target.Type === "TRACKED_CLUSTER") {
183 observedClusters.push(id);
184 } else if (target.Type === "RDS_INSTANCE") {
185 observedInstances.push(id);
186 }
187 }
188 return { observedClusters, observedInstances };
189 });
190
191 return {
192 stables: ["dbProxyName", "targetGroupArn", "targetGroupName"],
193 // Target groups are proxy-scoped: `describeDBProxyTargetGroups`
194 // requires a `DBProxyName`, so there is no account-wide enumeration.
195 // Fan out — enumerate every proxy via the paginated `describeDBProxies`,
196 // then list each proxy's target groups and hydrate registered targets
197 // (cluster/instance identifiers) via `describeDBProxyTargets` so each
198 // element matches `read`'s `Attributes` shape exactly. A proxy/target
199 // group deleted between describes surfaces a typed not-found per item
200 // and is dropped.
201 list: () =>
202 Effect.gen(function* () {
203 const proxies = yield* rds.describeDBProxies.pages({}).pipe(
204 Stream.runCollect,
205 Effect.map((chunk) =>
206 Array.from(chunk).flatMap((page) => page.DBProxies ?? []),
207 ),
208 );
209 const perProxy = yield* Effect.forEach(
210 proxies,
211 (proxy) =>
212 Effect.gen(function* () {
213 const dbProxyName = proxy.DBProxyName;
214 if (!dbProxyName) return [];
215 const groups = yield* rds.describeDBProxyTargetGroups
216 .pages({ DBProxyName: dbProxyName })
217 .pipe(
218 Stream.runCollect,
219 Effect.map((chunk) =>
220 Array.from(chunk).flatMap(
221 (page) => page.TargetGroups ?? [],
222 ),
223 ),
224 Effect.catchTag("DBProxyNotFoundFault", () =>
225 Effect.succeed([] as rds.DBProxyTargetGroup[]),
226 ),
227 Effect.catchTag("DBProxyTargetGroupNotFoundFault", () =>
228 Effect.succeed([] as rds.DBProxyTargetGroup[]),
229 ),
230 );
231 return yield* Effect.forEach(
232 groups,
233 (group) =>
234 Effect.gen(function* () {
235 const targetGroupName =
236 group.TargetGroupName ?? "default";
237 const { observedClusters, observedInstances } =
238 yield* readTargets({ dbProxyName, targetGroupName });
239 return {
240 dbProxyName: group.DBProxyName ?? dbProxyName,
241 targetGroupName,
242 targetGroupArn: group.TargetGroupArn,
243 status: group.Status,
244 isDefault: group.IsDefault,
245 connectionPoolConfig: group.ConnectionPoolConfig,
246 dbClusterIdentifiers: observedClusters,
247 dbInstanceIdentifiers: observedInstances,
248 } satisfies DBProxyTargetGroup["Attributes"];
249 }),
250 { concurrency: 10 },
251 );
252 }),
253 { concurrency: 10 },
254 );
255 return perProxy.flat();
256 }),
257 diff: Effect.fn(function* ({ olds, news }) {
258 if (!isResolved(news)) return undefined;
259 if (olds?.dbProxyName !== news.dbProxyName) {
260 return { action: "replace" } as const;
261 }
262 if (toTargetGroupName(olds ?? news) !== toTargetGroupName(news)) {
263 return { action: "replace" } as const;
264 }
265 }),
266 read: Effect.fn(function* ({ olds, output }) {
267 const props = {
268 dbProxyName: output?.dbProxyName ?? olds?.dbProxyName ?? "",
269 targetGroupName: output?.targetGroupName ?? olds?.targetGroupName,
270 dbClusterIdentifiers:
271 output?.dbClusterIdentifiers ?? olds?.dbClusterIdentifiers,
272 dbInstanceIdentifiers:
273 output?.dbInstanceIdentifiers ?? olds?.dbInstanceIdentifiers,
274 connectionPoolConfig:
275 output?.connectionPoolConfig ?? olds?.connectionPoolConfig,
276 } satisfies DBProxyTargetGroupProps;
277 const group = yield* readGroup({
278 dbProxyName: props.dbProxyName,
279 targetGroupName: toTargetGroupName(props),
280 });
281 if (!group?.TargetGroupName) {
282 return undefined;
283 }
284 return toAttrs({ group, props });
285 }),
286 reconcile: Effect.fn(function* ({ news, output, session }) {
287 const targetGroupName = toTargetGroupName(news);
288
289 // Observe — the "default" target group is created automatically
290 // by RDS when the parent DBProxy is created, so we don't `Ensure`
291 // a target group ourselves. We just describe it to confirm the
292 // proxy is ready and to read connection-pool config.
293 let observedGroup = yield* readGroup({
294 dbProxyName: news.dbProxyName,
295 targetGroupName,
296 });
297
298 // Sync connection pool config — push desired shape if provided.
299 // `modifyDBProxyTargetGroup` is idempotent for unchanged config.
300 if (news.connectionPoolConfig) {
301 yield* rds.modifyDBProxyTargetGroup({
302 DBProxyName: news.dbProxyName,
303 TargetGroupName: targetGroupName,
304 ConnectionPoolConfig: news.connectionPoolConfig,
305 });
306 }
307
308 // Sync registered targets — diff observed live targets against
309 // desired and apply only the delta. We trust live cloud state
310 // over `output`/`olds` so adoption and drift converge.
311 const { observedClusters, observedInstances } = yield* readTargets({
312 dbProxyName: news.dbProxyName,
313 targetGroupName,
314 });
315 const desiredClusters = new Set(news.dbClusterIdentifiers ?? []);
316 const desiredInstances = new Set(news.dbInstanceIdentifiers ?? []);
317 const observedClusterSet = new Set(observedClusters);
318 const observedInstanceSet = new Set(observedInstances);
319
320 const addClusters = [...desiredClusters].filter(
321 (id) => !observedClusterSet.has(id),
322 );
323 const removeClusters = [...observedClusterSet].filter(
324 (id) => !desiredClusters.has(id),
325 );
326 const addInstances = [...desiredInstances].filter(
327 (id) => !observedInstanceSet.has(id),
328 );
329 const removeInstances = [...observedInstanceSet].filter(
330 (id) => !desiredInstances.has(id),
331 );
332
333 if (addClusters.length > 0 || addInstances.length > 0) {
334 yield* rds.registerDBProxyTargets({
335 DBProxyName: news.dbProxyName,
336 TargetGroupName: targetGroupName,
337 DBClusterIdentifiers:
338 addClusters.length > 0 ? addClusters : undefined,
339 DBInstanceIdentifiers:
340 addInstances.length > 0 ? addInstances : undefined,
341 });
342 }
343
344 if (removeClusters.length > 0 || removeInstances.length > 0) {
345 yield* rds.deregisterDBProxyTargets({
346 DBProxyName: news.dbProxyName,
347 TargetGroupName: targetGroupName,
348 DBClusterIdentifiers:
349 removeClusters.length > 0 ? removeClusters : undefined,
350 DBInstanceIdentifiers:
351 removeInstances.length > 0 ? removeInstances : undefined,
352 });
353 }
354
355 // Re-read so returned attrs reflect post-sync state. If the
356 // target group still isn't visible, surface the failure (the
357 // parent DBProxy must be up).
358 observedGroup = yield* readGroup({
359 dbProxyName: news.dbProxyName,
360 targetGroupName,
361 });
362 if (!observedGroup?.TargetGroupName) {
363 return yield* Effect.fail(
364 new Error(`DB proxy target group '${targetGroupName}' not found`),
365 );
366 }
367 yield* session.note(
368 observedGroup.TargetGroupArn ??
369 output?.targetGroupArn ??
370 observedGroup.TargetGroupName,
371 );
372 return toAttrs({ group: observedGroup, props: news });
373 }),
374 delete: Effect.fn(function* ({ output }) {
375 if (
376 output.dbClusterIdentifiers.length > 0 ||
377 output.dbInstanceIdentifiers.length > 0
378 ) {
379 yield* rds
380 .deregisterDBProxyTargets({
381 DBProxyName: output.dbProxyName,
382 TargetGroupName: output.targetGroupName,
383 DBClusterIdentifiers:
384 output.dbClusterIdentifiers.length > 0
385 ? output.dbClusterIdentifiers
386 : undefined,
387 DBInstanceIdentifiers:
388 output.dbInstanceIdentifiers.length > 0
389 ? output.dbInstanceIdentifiers
390 : undefined,
391 })
392 .pipe(
393 Effect.catchTag(
394 "DBProxyTargetGroupNotFoundFault",
395 () => Effect.void,
396 ),
397 );
398 }
399 }),
400 };
401 }),
402 );