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 { createPhysicalName } from "../../PhysicalName.ts";
6import * as Provider from "../../Provider.ts";
7import { Resource } from "../../Resource.ts";
8import type { Providers } from "../Providers.ts";
9import { createInternalTags, diffTags } from "../../Tags.ts";
10
11export interface DBClusterEndpointProps {
12 /**
13 * Endpoint identifier. If omitted, Alchemy generates one.
14 */
15 dbClusterEndpointIdentifier?: string;
16 /**
17 * Cluster that owns the endpoint.
18 */
19 dbClusterIdentifier: string;
20 /**
21 * Endpoint type such as `READER`, `WRITER`, or `ANY`.
22 */
23 endpointType: string;
24 /**
25 * Static members explicitly attached to the endpoint.
26 */
27 staticMembers?: string[];
28 /**
29 * Members excluded from the endpoint.
30 */
31 excludedMembers?: string[];
32 /**
33 * User-defined tags.
34 */
35 tags?: Record<string, string>;
36}
37
38export interface DBClusterEndpoint extends Resource<
39 "AWS.RDS.DBClusterEndpoint",
40 DBClusterEndpointProps,
41 {
42 /**
43 * Identifier of the custom endpoint.
44 */
45 dbClusterEndpointIdentifier: string;
46 /**
47 * ARN of the custom endpoint.
48 */
49 dbClusterEndpointArn: string | undefined;
50 /**
51 * Cluster that owns the endpoint.
52 */
53 dbClusterIdentifier: string | undefined;
54 /**
55 * DNS address applications connect to.
56 */
57 endpoint: string | undefined;
58 /**
59 * Status of the endpoint (e.g. `available`).
60 */
61 status: string | undefined;
62 /**
63 * Endpoint type (`CUSTOM` for managed endpoints).
64 */
65 endpointType: string | undefined;
66 /**
67 * Traffic routing type of the custom endpoint (`READER`, `ANY`).
68 */
69 customEndpointType: string | undefined;
70 /**
71 * Instances explicitly attached to the endpoint.
72 */
73 staticMembers: string[];
74 /**
75 * Instances excluded from the endpoint.
76 */
77 excludedMembers: string[];
78 /**
79 * Tags on the endpoint.
80 */
81 tags: Record<string, string>;
82 },
83 never,
84 Providers
85> {}
86
87/**
88 * A custom Aurora cluster endpoint — a DNS name that routes to a chosen
89 * subset of a cluster's instances, on top of the built-in writer and reader
90 * endpoints of a `DBCluster`.
91 *
92 * Use it to pin analytics traffic to specific readers or to keep a stable
93 * address across instance replacements. Changing the identifier or owning
94 * cluster replaces the endpoint; type and membership update in place.
95 * ### Creating Custom Endpoints
96 * **Example:** Reader Endpoint for a Cluster
97 * ```typescript
98 * const readers = yield* DBClusterEndpoint("Readers", {
99 * dbClusterIdentifier: cluster.dbClusterIdentifier,
100 * endpointType: "READER",
101 * });
102 * ```
103 *
104 * **Example:** Pin Specific Instances
105 * ```typescript
106 * const analytics = yield* DBClusterEndpoint("Analytics", {
107 * dbClusterIdentifier: cluster.dbClusterIdentifier,
108 * endpointType: "ANY",
109 * staticMembers: [reporting.dbInstanceIdentifier],
110 * });
111 * ```
112 *
113 * @resource
114 */
115export const DBClusterEndpoint = Resource<DBClusterEndpoint>(
116 "AWS.RDS.DBClusterEndpoint",
117);
118
119const toAttrs = ({
120 endpoint,
121 tags,
122}: {
123 endpoint: rds.DBClusterEndpoint;
124 tags: Record<string, string>;
125}): DBClusterEndpoint["Attributes"] => ({
126 dbClusterEndpointIdentifier: endpoint.DBClusterEndpointIdentifier ?? "",
127 dbClusterEndpointArn: endpoint.DBClusterEndpointArn,
128 dbClusterIdentifier: endpoint.DBClusterIdentifier,
129 endpoint: endpoint.Endpoint,
130 status: endpoint.Status,
131 endpointType: endpoint.EndpointType,
132 customEndpointType: endpoint.CustomEndpointType,
133 staticMembers: endpoint.StaticMembers ?? [],
134 excludedMembers: endpoint.ExcludedMembers ?? [],
135 tags,
136});
137
138export const DBClusterEndpointProvider = () =>
139 Provider.effect(
140 DBClusterEndpoint,
141 Effect.gen(function* () {
142 const toIdentifier = (id: string, props: DBClusterEndpointProps) =>
143 props.dbClusterEndpointIdentifier
144 ? Effect.succeed(props.dbClusterEndpointIdentifier)
145 : createPhysicalName({ id, maxLength: 63 });
146
147 const readEndpoint = Effect.fn(function* (identifier: string) {
148 const response = yield* rds
149 .describeDBClusterEndpoints({
150 DBClusterEndpointIdentifier: identifier,
151 })
152 .pipe(
153 Effect.catchTag("DBClusterNotFoundFault", () =>
154 Effect.succeed(undefined),
155 ),
156 );
157 return response?.DBClusterEndpoints?.[0];
158 });
159
160 return {
161 stables: ["dbClusterEndpointArn", "dbClusterEndpointIdentifier"],
162 // Enumerate every custom cluster endpoint in the account/region.
163 // `describeDBClusterEndpoints` with no filter returns custom endpoints
164 // across all clusters; system endpoints (EndpointType WRITER/READER)
165 // aren't managed via createDBClusterEndpoint, so we keep only CUSTOM
166 // ones. Tags aren't returned by describe, so each item carries `{}`
167 // (matching `read`'s default when there is no recorded tag set).
168 list: () =>
169 rds.describeDBClusterEndpoints.pages({}).pipe(
170 Stream.runCollect,
171 Effect.map((chunk) =>
172 Array.from(chunk).flatMap((page) =>
173 (page.DBClusterEndpoints ?? [])
174 .filter((endpoint) => endpoint.EndpointType === "CUSTOM")
175 .map((endpoint) => toAttrs({ endpoint, tags: {} })),
176 ),
177 ),
178 ),
179 diff: Effect.fn(function* ({ id, olds, news }) {
180 if (!isResolved(news)) return;
181 if (
182 (yield* toIdentifier(
183 id,
184 olds ?? ({} as DBClusterEndpointProps),
185 )) !== (yield* toIdentifier(id, news))
186 ) {
187 return { action: "replace" } as const;
188 }
189 if (olds?.dbClusterIdentifier !== news.dbClusterIdentifier) {
190 return { action: "replace" } as const;
191 }
192 }),
193 read: Effect.fn(function* ({ id, olds, output }) {
194 const identifier =
195 output?.dbClusterEndpointIdentifier ??
196 (yield* toIdentifier(
197 id,
198 olds ??
199 ({
200 dbClusterIdentifier: "",
201 endpointType: "READER",
202 } as DBClusterEndpointProps),
203 ));
204 const endpoint = yield* readEndpoint(identifier);
205 if (!endpoint?.DBClusterEndpointIdentifier) {
206 return undefined;
207 }
208 return toAttrs({ endpoint, tags: output?.tags ?? {} });
209 }),
210 reconcile: Effect.fn(function* ({ id, news, output, session }) {
211 const identifier =
212 output?.dbClusterEndpointIdentifier ??
213 (yield* toIdentifier(id, news));
214 const internalTags = yield* createInternalTags(id);
215 const desiredTags = { ...internalTags, ...news.tags };
216
217 // Observe — fetch the endpoint's live state.
218 let observed = yield* readEndpoint(identifier);
219
220 // Ensure — create if missing. Tolerate
221 // `DBClusterEndpointAlreadyExistsFault` as a race with a peer
222 // reconciler.
223 if (!observed?.DBClusterEndpointIdentifier) {
224 yield* rds
225 .createDBClusterEndpoint({
226 DBClusterIdentifier: news.dbClusterIdentifier,
227 DBClusterEndpointIdentifier: identifier,
228 EndpointType: news.endpointType,
229 StaticMembers: news.staticMembers,
230 ExcludedMembers: news.excludedMembers,
231 Tags: Object.entries(desiredTags).map(([Key, Value]) => ({
232 Key,
233 Value,
234 })),
235 })
236 .pipe(
237 Effect.catchTag(
238 "DBClusterEndpointAlreadyExistsFault",
239 () => Effect.void,
240 ),
241 );
242 observed = yield* readEndpoint(identifier);
243 if (!observed?.DBClusterEndpointIdentifier) {
244 return yield* Effect.fail(
245 new Error(
246 `DB cluster endpoint '${identifier}' not found after create`,
247 ),
248 );
249 }
250 } else {
251 // Sync mutable endpoint config — push desired shape.
252 yield* rds.modifyDBClusterEndpoint({
253 DBClusterEndpointIdentifier: identifier,
254 EndpointType: news.endpointType,
255 StaticMembers: news.staticMembers,
256 ExcludedMembers: news.excludedMembers,
257 });
258 observed = yield* readEndpoint(identifier);
259 if (!observed?.DBClusterEndpointIdentifier) {
260 return yield* Effect.fail(
261 new Error(
262 `DB cluster endpoint '${identifier}' not found after update`,
263 ),
264 );
265 }
266 }
267
268 const dbClusterEndpointArn = observed.DBClusterEndpointArn;
269
270 // Sync tags — diff observed cloud tags against desired. The
271 // describeDBClusterEndpoints response does not include tags, so we
272 // diff against the previously-recorded tag set on `output`.
273 const observedTags = output?.tags ?? {};
274 const { removed, upsert } = diffTags(observedTags, desiredTags);
275 if (upsert.length > 0 && dbClusterEndpointArn) {
276 yield* rds.addTagsToResource({
277 ResourceName: dbClusterEndpointArn,
278 Tags: upsert,
279 });
280 }
281 if (removed.length > 0 && dbClusterEndpointArn) {
282 yield* rds.removeTagsFromResource({
283 ResourceName: dbClusterEndpointArn,
284 TagKeys: removed,
285 });
286 }
287
288 yield* session.note(dbClusterEndpointArn ?? identifier);
289 return toAttrs({ endpoint: observed, tags: desiredTags });
290 }),
291 delete: Effect.fn(function* ({ output }) {
292 yield* rds
293 .deleteDBClusterEndpoint({
294 DBClusterEndpointIdentifier: output.dbClusterEndpointIdentifier,
295 })
296 .pipe(
297 Effect.catchTag(
298 "DBClusterEndpointNotFoundFault",
299 () => Effect.void,
300 ),
301 );
302 }),
303 };
304 }),
305 );