| 1 | import * as rds from "@distilled.cloud/aws/rds"; |
| 2 | import * as Effect from "effect/Effect"; |
| 3 | import * as Stream from "effect/Stream"; |
| 4 | import { isResolved } from "../../Diff.ts"; |
| 5 | import { createPhysicalName } from "../../PhysicalName.ts"; |
| 6 | import * as Provider from "../../Provider.ts"; |
| 7 | import { Resource } from "../../Resource.ts"; |
| 8 | import type { Providers } from "../Providers.ts"; |
| 9 | import { createInternalTags, diffTags } from "../../Tags.ts"; |
| 10 | |
| 11 | export 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 | |
| 38 | export 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 | */ |
| 115 | export const DBClusterEndpoint = Resource<DBClusterEndpoint>( |
| 116 | "AWS.RDS.DBClusterEndpoint", |
| 117 | ); |
| 118 | |
| 119 | const 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 | |
| 138 | export 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 | ); |