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