1import * as rds from "@distilled.cloud/aws/rds";
2import * as Effect from "effect/Effect";
3import * as Schedule from "effect/Schedule";
4import * as Stream from "effect/Stream";
5import { isResolved } from "../../Diff.ts";
6import { createPhysicalName } from "../../PhysicalName.ts";
7import * as Provider from "../../Provider.ts";
8import { Resource } from "../../Resource.ts";
9import type { Providers } from "../Providers.ts";
10import { createInternalTags, diffTags } from "../../Tags.ts";
11
12export interface DBProxyEndpointProps {
13 /**
14 * Proxy endpoint name. If omitted, Alchemy generates one.
15 */
16 dbProxyEndpointName?: string;
17 /**
18 * Proxy that owns the endpoint.
19 */
20 dbProxyName: string;
21 /**
22 * Subnets used by the proxy endpoint.
23 */
24 vpcSubnetIds: string[];
25 /**
26 * Security groups attached to the endpoint.
27 */
28 vpcSecurityGroupIds?: string[];
29 /**
30 * Target role for the endpoint.
31 */
32 targetRole?: rds.DBProxyEndpointTargetRole;
33 /**
34 * Endpoint network type.
35 */
36 endpointNetworkType?: rds.EndpointNetworkType;
37 /**
38 * User-defined tags.
39 */
40 tags?: Record<string, string>;
41}
42
43export interface DBProxyEndpoint extends Resource<
44 "AWS.RDS.DBProxyEndpoint",
45 DBProxyEndpointProps,
46 {
47 /**
48 * Name of the proxy endpoint.
49 */
50 dbProxyEndpointName: string;
51 /**
52 * ARN of the proxy endpoint.
53 */
54 dbProxyEndpointArn: string;
55 /**
56 * Proxy that owns the endpoint.
57 */
58 dbProxyName: string | undefined;
59 /**
60 * DNS address applications connect to.
61 */
62 endpoint: string | undefined;
63 /**
64 * Status of the endpoint (e.g. `available`).
65 */
66 status: string | undefined;
67 /**
68 * VPC the endpoint is placed in.
69 */
70 vpcId: string | undefined;
71 /**
72 * Subnets the endpoint is attached to.
73 */
74 vpcSubnetIds: string[];
75 /**
76 * Security groups attached to the endpoint.
77 */
78 vpcSecurityGroupIds: string[];
79 /**
80 * Role of the endpoint (`READ_WRITE` or `READ_ONLY`).
81 */
82 targetRole: string | undefined;
83 /**
84 * Tags on the endpoint.
85 */
86 tags: Record<string, string>;
87 },
88 never,
89 Providers
90> {}
91
92/**
93 * An additional RDS Proxy endpoint — a second DNS name on an existing
94 * `DBProxy`, typically read-only for reader traffic or placed in a
95 * different VPC.
96 *
97 * Changing the name, owning proxy, subnets, or target role replaces the
98 * endpoint; security groups and tags update in place.
99 * ### Creating Proxy Endpoints
100 * **Example:** Read-Only Endpoint
101 * ```typescript
102 * const readerEndpoint = yield* DBProxyEndpoint("ReaderEndpoint", {
103 * dbProxyName: proxy.dbProxyName,
104 * vpcSubnetIds: [privateSubnetA.subnetId, privateSubnetB.subnetId],
105 * vpcSecurityGroupIds: [dbSecurityGroup.groupId],
106 * targetRole: "READ_ONLY",
107 * });
108 * ```
109 *
110 * @resource
111 */
112export const DBProxyEndpoint = Resource<DBProxyEndpoint>(
113 "AWS.RDS.DBProxyEndpoint",
114);
115
116const toAttrs = ({
117 endpoint,
118 tags,
119}: {
120 endpoint: rds.DBProxyEndpoint;
121 tags: Record<string, string>;
122}): DBProxyEndpoint["Attributes"] => ({
123 dbProxyEndpointName: endpoint.DBProxyEndpointName ?? "",
124 dbProxyEndpointArn: endpoint.DBProxyEndpointArn ?? "",
125 dbProxyName: endpoint.DBProxyName,
126 endpoint: endpoint.Endpoint,
127 status: endpoint.Status,
128 vpcId: endpoint.VpcId,
129 vpcSubnetIds: endpoint.VpcSubnetIds ?? [],
130 vpcSecurityGroupIds: endpoint.VpcSecurityGroupIds ?? [],
131 targetRole: endpoint.TargetRole,
132 tags,
133});
134
135export const DBProxyEndpointProvider = () =>
136 Provider.effect(
137 DBProxyEndpoint,
138 Effect.gen(function* () {
139 const toName = (id: string, props: DBProxyEndpointProps) =>
140 props.dbProxyEndpointName
141 ? Effect.succeed(props.dbProxyEndpointName)
142 : createPhysicalName({ id, maxLength: 63 });
143
144 const readEndpoint = Effect.fn(function* ({
145 dbProxyName,
146 dbProxyEndpointName,
147 }: {
148 dbProxyName: string;
149 dbProxyEndpointName: string;
150 }) {
151 const response = yield* rds
152 .describeDBProxyEndpoints({
153 DBProxyName: dbProxyName,
154 DBProxyEndpointName: dbProxyEndpointName,
155 })
156 .pipe(
157 Effect.catchTag("DBProxyEndpointNotFoundFault", () =>
158 Effect.succeed(undefined),
159 ),
160 );
161 return response?.DBProxyEndpoints?.[0];
162 });
163
164 const waitForEndpoint = Effect.fn(function* (props: {
165 dbProxyName: string;
166 dbProxyEndpointName: string;
167 }) {
168 const readinessPolicy = Schedule.max([
169 Schedule.fixed("2 seconds"),
170 Schedule.recurs(30),
171 ]);
172 return yield* readEndpoint(props).pipe(
173 Effect.flatMap((endpoint) =>
174 endpoint?.DBProxyEndpointArn
175 ? Effect.succeed(endpoint)
176 : Effect.fail(
177 new Error(
178 `DB proxy endpoint '${props.dbProxyEndpointName}' not ready`,
179 ),
180 ),
181 ),
182 Effect.retry({ schedule: readinessPolicy }),
183 );
184 });
185
186 return {
187 stables: ["dbProxyEndpointArn", "dbProxyEndpointName"],
188 list: () =>
189 Effect.gen(function* () {
190 // Endpoints are keyed under a parent proxy. Enumerate every proxy,
191 // then fan out `describeDBProxyEndpoints` per proxy (bounded
192 // concurrency) and flatten. `describe` does not surface tags
193 // inline, so each item hydrates with `tags: {}` — the same shape
194 // `read` returns when no prior tags are recorded.
195 const proxyNames = yield* rds.describeDBProxies.pages({}).pipe(
196 Stream.runCollect,
197 Effect.map((chunk) =>
198 Array.from(chunk).flatMap((page) =>
199 (page.DBProxies ?? [])
200 .map((proxy) => proxy.DBProxyName)
201 .filter((name): name is string => name != null),
202 ),
203 ),
204 );
205 const rows = yield* Effect.forEach(
206 proxyNames,
207 (dbProxyName) =>
208 rds.describeDBProxyEndpoints
209 .pages({ DBProxyName: dbProxyName })
210 .pipe(
211 Stream.runCollect,
212 Effect.map((chunk) =>
213 Array.from(chunk).flatMap((page) =>
214 (page.DBProxyEndpoints ?? []).map((endpoint) =>
215 toAttrs({ endpoint, tags: {} }),
216 ),
217 ),
218 ),
219 // A proxy (or its endpoints) may be deleted mid-enumeration.
220 Effect.catchTag(
221 ["DBProxyNotFoundFault", "DBProxyEndpointNotFoundFault"],
222 () =>
223 Effect.succeed([] as DBProxyEndpoint["Attributes"][]),
224 ),
225 ),
226 { concurrency: 10 },
227 );
228 return rows.flat();
229 }),
230 diff: Effect.fn(function* ({ id, olds, news }) {
231 if (!isResolved(news)) return undefined;
232 if (
233 (yield* toName(id, olds ?? ({} as DBProxyEndpointProps))) !==
234 (yield* toName(id, news))
235 ) {
236 return { action: "replace" } as const;
237 }
238 if (olds?.dbProxyName !== news.dbProxyName) {
239 return { action: "replace" } as const;
240 }
241 }),
242 read: Effect.fn(function* ({ id, olds, output }) {
243 const dbProxyEndpointName =
244 output?.dbProxyEndpointName ??
245 (yield* toName(
246 id,
247 olds ??
248 ({
249 dbProxyName: "",
250 vpcSubnetIds: [],
251 } as DBProxyEndpointProps),
252 ));
253 const endpoint = yield* readEndpoint({
254 dbProxyName: output?.dbProxyName ?? olds?.dbProxyName ?? "",
255 dbProxyEndpointName,
256 });
257 if (!endpoint?.DBProxyEndpointArn) {
258 return undefined;
259 }
260 return toAttrs({ endpoint, tags: output?.tags ?? {} });
261 }),
262 reconcile: Effect.fn(function* ({ id, news, output, session }) {
263 const dbProxyEndpointName =
264 output?.dbProxyEndpointName ?? (yield* toName(id, news));
265 const internalTags = yield* createInternalTags(id);
266 const desiredTags = { ...internalTags, ...news.tags };
267
268 // Observe — fetch live endpoint state.
269 let observed = yield* readEndpoint({
270 dbProxyName: output?.dbProxyName ?? news.dbProxyName,
271 dbProxyEndpointName,
272 });
273
274 // Ensure — create if missing. Tolerate
275 // `DBProxyEndpointAlreadyExistsFault` as a race with a peer
276 // reconciler.
277 if (!observed?.DBProxyEndpointArn) {
278 yield* rds
279 .createDBProxyEndpoint({
280 DBProxyName: news.dbProxyName,
281 DBProxyEndpointName: dbProxyEndpointName,
282 VpcSubnetIds: news.vpcSubnetIds,
283 VpcSecurityGroupIds: news.vpcSecurityGroupIds,
284 TargetRole: news.targetRole,
285 EndpointNetworkType: news.endpointNetworkType,
286 Tags: Object.entries(desiredTags).map(([Key, Value]) => ({
287 Key,
288 Value,
289 })),
290 })
291 .pipe(
292 Effect.catchTag(
293 "DBProxyEndpointAlreadyExistsFault",
294 () => Effect.void,
295 ),
296 );
297
298 observed = yield* waitForEndpoint({
299 dbProxyName: news.dbProxyName,
300 dbProxyEndpointName,
301 });
302 } else {
303 // Sync mutable endpoint config — security groups + rename. The
304 // rename argument is a no-op when desired matches observed.
305 yield* rds.modifyDBProxyEndpoint({
306 DBProxyEndpointName: dbProxyEndpointName,
307 VpcSecurityGroupIds: news.vpcSecurityGroupIds,
308 NewDBProxyEndpointName:
309 news.dbProxyEndpointName &&
310 news.dbProxyEndpointName !== dbProxyEndpointName
311 ? news.dbProxyEndpointName
312 : undefined,
313 });
314 observed = yield* waitForEndpoint({
315 dbProxyName: observed.DBProxyName ?? news.dbProxyName,
316 dbProxyEndpointName:
317 news.dbProxyEndpointName &&
318 news.dbProxyEndpointName !== dbProxyEndpointName
319 ? news.dbProxyEndpointName
320 : dbProxyEndpointName,
321 });
322 }
323
324 const dbProxyEndpointArn = observed.DBProxyEndpointArn ?? "";
325
326 // Sync tags — diff prior recorded tags against desired (describe
327 // does not surface tags inline).
328 const observedTags = output?.tags ?? {};
329 const { removed, upsert } = diffTags(observedTags, desiredTags);
330 if (upsert.length > 0 && dbProxyEndpointArn) {
331 yield* rds.addTagsToResource({
332 ResourceName: dbProxyEndpointArn,
333 Tags: upsert,
334 });
335 }
336 if (removed.length > 0 && dbProxyEndpointArn) {
337 yield* rds.removeTagsFromResource({
338 ResourceName: dbProxyEndpointArn,
339 TagKeys: removed,
340 });
341 }
342
343 yield* session.note(dbProxyEndpointArn || dbProxyEndpointName);
344 return toAttrs({ endpoint: observed, tags: desiredTags });
345 }),
346 delete: Effect.fn(function* ({ output }) {
347 yield* rds
348 .deleteDBProxyEndpoint({
349 DBProxyEndpointName: output.dbProxyEndpointName,
350 })
351 .pipe(
352 Effect.catchTag(
353 "DBProxyEndpointNotFoundFault",
354 () => Effect.void,
355 ),
356 );
357 }),
358 };
359 }),
360 );