Skip to content
Source
Sign inSign up
Open History to see the last commit

DBSubnetGroup.ts

279 lines10.2 KB

1import * as rds from "@distilled.cloud/aws/rds";
2import { isResolved } from "alchemy/Diff";
3import { createPhysicalName } from "alchemy/PhysicalName";
4import * as Provider from "alchemy/Provider";
5import { Resource } from "alchemy/Resource";
6import { createInternalTags, diffTags } from "alchemy/Tags";
7import * as Data from "effect/Data";
8import * as Effect from "effect/Effect";
9import * as Stream from "effect/Stream";
10import type { SubnetId } from "../EC2/Subnet.ts";
11import type { Providers } from "../Providers.ts";
12​
13export class RDSDBSubnetGroupUnexpectedResponse extends Data.TaggedError(
14 "RDSDBSubnetGroupUnexpectedResponse",
15)<{
16 readonly message: string;
17 readonly cause?: unknown;
18}> {}
19​
20export interface DBSubnetGroupProps {
21 /**
22 * Name of the DB subnet group. If omitted, a deterministic name is generated.
23 */
24 dbSubnetGroupName?: string;
25 /**
26 * Description for the subnet group.
27 * @default "Managed by Alchemy"
28 */
29 description?: string;
30 /**
31 * Subnets that the database resources may use.
32 */
33 subnetIds: SubnetId[];
34 /**
35 * User-defined tags for the subnet group.
36 */
37 tags?: Record<string, string>;
39​
40export interface DBSubnetGroup extends Resource<
41 "AWS.RDS.DBSubnetGroup",
42 DBSubnetGroupProps,
43 {
44 /**
45 * Name of the subnet group.
46 */
47 dbSubnetGroupName: string;
48 /**
49 * ARN of the subnet group.
50 */
51 dbSubnetGroupArn: string | undefined;
52 /**
53 * VPC the subnets belong to.
54 */
55 vpcId: string | undefined;
56 /**
57 * Subnet IDs registered in the group.
58 */
59 subnetIds: string[];
60 /**
61 * Status of the subnet group (e.g. `Complete`).
62 */
63 status: string | undefined;
64 /**
65 * Network types the group supports (`IPV4`, `DUAL`).
66 */
67 supportedNetworkTypes: string[] | undefined;
68 /**
69 * Tags on the subnet group.
70 */
71 tags: Record<string, string>;
72 },
73 never,
74 Providers
75> {}
76​
77/**
78 * An RDS DB subnet group for Aurora clusters, instances, and proxies.
79 *
80 * RDS requires a subnet group spanning at least two Availability Zones
81 * before a cluster or instance can be placed in a VPC. Changing the name
82 * replaces the group; the subnet list updates in place.
83 * ### Creating a Subnet Group
84 * **Example:** Subnet Group Across Two AZs
85 * ```typescript
86 * const subnetGroup = yield* DBSubnetGroup("SubnetGroup", {
87 * subnetIds: [privateSubnetA.subnetId, privateSubnetB.subnetId],
88 * });
89 * ```
90 *
91 * **Example:** Place an Aurora Cluster in the Group
92 * ```typescript
93 * const cluster = yield* DBCluster("Cluster", {
94 * engine: "aurora-postgresql",
95 * dbSubnetGroupName: subnetGroup.dbSubnetGroupName,
96 * vpcSecurityGroupIds: [dbSecurityGroup.groupId],
97 * });
98 * ```
99 *
100 * @resource
101 */
102export const DBSubnetGroup = Resource<DBSubnetGroup>("AWS.RDS.DBSubnetGroup");
103​
104export const DBSubnetGroupProvider = () =>
105 Provider.effect(
106 DBSubnetGroup,
107 Effect.gen(function* () {
108 const toName = (id: string, props: DBSubnetGroupProps) =>
109 props.dbSubnetGroupName
110 ? Effect.succeed(props.dbSubnetGroupName)
111 : createPhysicalName({ id, maxLength: 255 });
112​
113 const readTags = Effect.fn(function* (arn: string | undefined) {
114 if (!arn) return {} as Record<string, string>;
115 const response = yield* rds.listTagsForResource({ ResourceName: arn });
116 return Object.fromEntries(
117 (response.TagList ?? []).flatMap(({ Key, Value }) =>
118 Key !== undefined && Value !== undefined ? [[Key, Value]] : [],
119 ),
120 ) as Record<string, string>;
121 });
122​
123 const readGroup = Effect.fn(function* (groupName: string) {
124 const response = yield* rds
125 .describeDBSubnetGroups({
126 DBSubnetGroupName: groupName,
127 })
128 .pipe(Effect.catchTag("DBSubnetGroupNotFoundFault", () => Effect.undefined));
129 return response?.DBSubnetGroups?.[0];
130 });
131​
132 return {
133 stables: ["dbSubnetGroupArn", "dbSubnetGroupName", "vpcId"],
134 diff: Effect.fn(function* ({ id, olds, news }) {
135 if (!isResolved(news)) return undefined;
136 if ((yield* toName(id, olds ?? {})) !== (yield* toName(id, news ?? {}))) {
137 return { action: "replace" } as const;
138 }
139 }),
140 read: Effect.fn(function* ({ id, olds, output }) {
141 const name =
142 output?.dbSubnetGroupName ??
143 (yield* toName(id, olds ?? ({ subnetIds: [] } as DBSubnetGroupProps)));
144 const group = yield* readGroup(name);
145 if (!group?.DBSubnetGroupName) {
146 return undefined;
147 }
148​
149 return {
150 dbSubnetGroupName: group.DBSubnetGroupName,
151 dbSubnetGroupArn: group.DBSubnetGroupArn,
152 vpcId: group.VpcId,
153 subnetIds: (group.Subnets ?? []).flatMap((subnet) =>
154 subnet.SubnetIdentifier ? [subnet.SubnetIdentifier] : [],
155 ),
156 status: group.SubnetGroupStatus,
157 supportedNetworkTypes: group.SupportedNetworkTypes,
158 tags: yield* readTags(group.DBSubnetGroupArn),
159 };
160 }),
161 reconcile: Effect.fn(function* ({ id, news, output, session }) {
162 const dbSubnetGroupName = output?.dbSubnetGroupName ?? (yield* toName(id, news));
163 const internalTags = yield* createInternalTags(id);
164 const desiredTags = { ...internalTags, ...news.tags };
165​
166 // Observe — fetch live subnet-group state.
167 let observed = yield* readGroup(dbSubnetGroupName);
168​
169 // Ensure — create if missing. Tolerate
170 // `DBSubnetGroupAlreadyExistsFault` as a race with a peer
171 // reconciler by re-reading.
172 if (!observed?.DBSubnetGroupName) {
173 yield* rds
174 .createDBSubnetGroup({
175 DBSubnetGroupName: dbSubnetGroupName,
176 DBSubnetGroupDescription: news.description ?? "Managed by Alchemy",
177 SubnetIds: news.subnetIds,
178 Tags: Object.entries(desiredTags).map(([Key, Value]) => ({
179 Key,
180 Value,
181 })),
182 })
183 .pipe(Effect.catchTag("DBSubnetGroupAlreadyExistsFault", () => Effect.void));
184 observed = yield* readGroup(dbSubnetGroupName);
185 if (!observed?.DBSubnetGroupName) {
186 return yield* new RDSDBSubnetGroupUnexpectedResponse({
187 message: `Failed to create DB subnet group '${dbSubnetGroupName}'`,
188 });
189 }
190 } else {
191 // Sync description and subnet membership — `modifyDBSubnetGroup`
192 // accepts the full desired set and is idempotent for unchanged
193 // input.
194 yield* rds.modifyDBSubnetGroup({
195 DBSubnetGroupName: dbSubnetGroupName,
196 DBSubnetGroupDescription: news.description ?? "Managed by Alchemy",
197 SubnetIds: news.subnetIds,
198 });
199 observed = yield* readGroup(dbSubnetGroupName);
200 if (!observed?.DBSubnetGroupName) {
201 return yield* new RDSDBSubnetGroupUnexpectedResponse({
202 message: `DB subnet group '${dbSubnetGroupName}' not found after update`,
203 });
204 }
205 }
206​
207 const dbSubnetGroupArn = observed.DBSubnetGroupArn;
208​
209 // Sync tags — diff observed cloud tags against desired (describe
210 // does not surface tags inline).
211 const observedTags = yield* readTags(dbSubnetGroupArn);
212 const { removed, upsert } = diffTags(observedTags, desiredTags);
213 if (upsert.length > 0 && dbSubnetGroupArn) {
214 yield* rds.addTagsToResource({
215 ResourceName: dbSubnetGroupArn,
216 Tags: upsert,
217 });
218 }
219 if (removed.length > 0 && dbSubnetGroupArn) {
220 yield* rds.removeTagsFromResource({
221 ResourceName: dbSubnetGroupArn,
222 TagKeys: removed,
223 });
224 }
225​
226 yield* session.note(dbSubnetGroupArn ?? dbSubnetGroupName);
227 return {
228 dbSubnetGroupName: observed.DBSubnetGroupName,
229 dbSubnetGroupArn,
230 vpcId: observed.VpcId,
231 subnetIds: (observed.Subnets ?? []).flatMap((subnet) =>
232 subnet.SubnetIdentifier ? [subnet.SubnetIdentifier] : [],
233 ),
234 status: observed.SubnetGroupStatus,
235 supportedNetworkTypes: observed.SupportedNetworkTypes,
236 tags: desiredTags,
237 };
238 }),
239 list: () =>
240 // AWS account/region collection: `describeDBSubnetGroups` is
241 // paginated (items field `DBSubnetGroups`). Collect every page and
242 // map each group into the `read` Attributes shape, with an empty
243 // tag map rather than a per-item `listTagsForResource` call.
244 rds.describeDBSubnetGroups.pages({}).pipe(
245 Stream.runCollect,
246 Effect.map((chunk) =>
247 Array.from(chunk).flatMap((page) =>
248 (page.DBSubnetGroups ?? []).flatMap((group) =>
249 // `default` is the RDS-family subnet group AWS auto-creates
250 // for the default VPC — never ours, never census/nuke it.
251 group.DBSubnetGroupName && group.DBSubnetGroupName !== "default"
252 ? [
253 {
254 dbSubnetGroupName: group.DBSubnetGroupName,
255 dbSubnetGroupArn: group.DBSubnetGroupArn,
256 vpcId: group.VpcId,
257 subnetIds: (group.Subnets ?? []).flatMap((subnet) =>
258 subnet.SubnetIdentifier ? [subnet.SubnetIdentifier] : [],
259 ),
260 status: group.SubnetGroupStatus,
261 supportedNetworkTypes: group.SupportedNetworkTypes,
262 tags: {} as Record<string, string>,
263 },
264 ]
265 : [],
266 ),
267 ),
268 ),
269 ),
270 delete: Effect.fn(function* ({ output }) {
271 yield* rds
272 .deleteDBSubnetGroup({
273 DBSubnetGroupName: output.dbSubnetGroupName,
274 })
275 .pipe(Effect.catchTag("DBSubnetGroupNotFoundFault", () => Effect.void));
276 }),
277 };
278 }),
279 );