1import * as docdb from "@distilled.cloud/aws/docdb";
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 the DocumentDB cluster may use. Must span at least two
24 * Availability Zones.
25 */
26 subnetIds: SubnetId[];
27 /**
28 * User-defined tags for the subnet group.
29 */
30 tags?: Record<string, string>;
31}
32
33export interface DBSubnetGroup extends Resource<
34 "AWS.DocDB.DBSubnetGroup",
35 DBSubnetGroupProps,
36 {
37 /** The subnet group name (unique per account/region). */
38 dbSubnetGroupName: string;
39 /** The ARN of the subnet group. */
40 dbSubnetGroupArn: string | undefined;
41 /** The VPC the subnets belong to. */
42 vpcId: string | undefined;
43 /** The IDs of the subnets in the group. */
44 subnetIds: string[];
45 /** The current status of the subnet group, e.g. `Complete`. */
46 status: string | undefined;
47 /** The network types the group supports, e.g. `IPV4`. */
48 supportedNetworkTypes: string[] | undefined;
49 /** The tags attached to the subnet group. */
50 tags: Record<string, string>;
51 },
52 never,
53 Providers
54> {}
55
56/**
57 * An Amazon DocumentDB subnet group — the set of VPC subnets a DocumentDB
58 * cluster and its instances are placed into. DocumentDB is VPC-only, so a
59 * subnet group spanning at least two Availability Zones is required before a
60 * cluster can be created.
61 * ### Creating a Subnet Group
62 * **Example:** Multi-AZ subnet group
63 * ```typescript
64 * const subnetGroup = yield* DBSubnetGroup("DocDbSubnets", {
65 * subnetIds: [subnetA.subnetId, subnetB.subnetId],
66 * });
67 * ```
68 *
69 * @resource
70 */
71export const DBSubnetGroup = Resource<DBSubnetGroup>("AWS.DocDB.DBSubnetGroup");
72
73export const DBSubnetGroupProvider = () =>
74 Provider.effect(
75 DBSubnetGroup,
76 Effect.gen(function* () {
77 const toName = (id: string, props: DBSubnetGroupProps) =>
78 props.dbSubnetGroupName
79 ? Effect.succeed(props.dbSubnetGroupName)
80 : createPhysicalName({ id, maxLength: 255 });
81
82 const readGroup = Effect.fn(function* (groupName: string) {
83 const response = yield* docdb
84 .describeDBSubnetGroups({
85 DBSubnetGroupName: groupName,
86 })
87 .pipe(
88 Effect.catchTag("DBSubnetGroupNotFoundFault", () =>
89 Effect.succeed(undefined),
90 ),
91 );
92 return response?.DBSubnetGroups?.[0];
93 });
94
95 return {
96 stables: ["dbSubnetGroupArn", "dbSubnetGroupName", "vpcId"],
97 diff: Effect.fn(function* ({ id, olds, news }) {
98 if (!isResolved(news)) return undefined;
99 if (
100 (yield* toName(id, olds ?? {})) !== (yield* toName(id, news ?? {}))
101 ) {
102 return { action: "replace" } as const;
103 }
104 }),
105 read: Effect.fn(function* ({ id, olds, output }) {
106 const name =
107 output?.dbSubnetGroupName ??
108 (yield* toName(
109 id,
110 olds ?? ({ subnetIds: [] } as DBSubnetGroupProps),
111 ));
112 const group = yield* readGroup(name);
113 if (!group?.DBSubnetGroupName) {
114 return undefined;
115 }
116
117 return {
118 dbSubnetGroupName: group.DBSubnetGroupName,
119 dbSubnetGroupArn: group.DBSubnetGroupArn,
120 vpcId: group.VpcId,
121 subnetIds: (group.Subnets ?? []).flatMap((subnet) =>
122 subnet.SubnetIdentifier ? [subnet.SubnetIdentifier] : [],
123 ),
124 status: group.SubnetGroupStatus,
125 supportedNetworkTypes: group.SupportedNetworkTypes,
126 tags: output?.tags ?? {},
127 };
128 }),
129 reconcile: Effect.fn(function* ({ id, news, output, session }) {
130 const dbSubnetGroupName =
131 output?.dbSubnetGroupName ?? (yield* toName(id, news));
132 const internalTags = yield* createInternalTags(id);
133 const desiredTags = { ...internalTags, ...news.tags };
134
135 // Observe — fetch live subnet-group state.
136 let observed = yield* readGroup(dbSubnetGroupName);
137
138 // Ensure — create if missing. Tolerate
139 // `DBSubnetGroupAlreadyExistsFault` as a race with a peer
140 // reconciler by re-reading.
141 if (!observed?.DBSubnetGroupName) {
142 yield* docdb
143 .createDBSubnetGroup({
144 DBSubnetGroupName: dbSubnetGroupName,
145 DBSubnetGroupDescription:
146 news.description ?? "Managed by Alchemy",
147 SubnetIds: news.subnetIds,
148 Tags: Object.entries(desiredTags).map(([Key, Value]) => ({
149 Key,
150 Value,
151 })),
152 })
153 .pipe(
154 Effect.catchTag(
155 "DBSubnetGroupAlreadyExistsFault",
156 () => Effect.void,
157 ),
158 );
159 observed = yield* readGroup(dbSubnetGroupName);
160 if (!observed?.DBSubnetGroupName) {
161 return yield* Effect.fail(
162 new Error(
163 `Failed to create DB subnet group '${dbSubnetGroupName}'`,
164 ),
165 );
166 }
167 } else {
168 // Sync description and subnet membership — `modifyDBSubnetGroup`
169 // accepts the full desired set and is idempotent for unchanged
170 // input.
171 yield* docdb.modifyDBSubnetGroup({
172 DBSubnetGroupName: dbSubnetGroupName,
173 DBSubnetGroupDescription:
174 news.description ?? "Managed by Alchemy",
175 SubnetIds: news.subnetIds,
176 });
177 observed = yield* readGroup(dbSubnetGroupName);
178 if (!observed?.DBSubnetGroupName) {
179 return yield* Effect.fail(
180 new Error(
181 `DB subnet group '${dbSubnetGroupName}' not found after update`,
182 ),
183 );
184 }
185 }
186
187 const dbSubnetGroupArn = observed.DBSubnetGroupArn;
188
189 // Sync tags — diff prior recorded tags against desired (describe
190 // does not surface tags inline).
191 const observedTags = output?.tags ?? {};
192 const { removed, upsert } = diffTags(observedTags, desiredTags);
193 if (upsert.length > 0 && dbSubnetGroupArn) {
194 yield* docdb.addTagsToResource({
195 ResourceName: dbSubnetGroupArn,
196 Tags: upsert,
197 });
198 }
199 if (removed.length > 0 && dbSubnetGroupArn) {
200 yield* docdb.removeTagsFromResource({
201 ResourceName: dbSubnetGroupArn,
202 TagKeys: removed,
203 });
204 }
205
206 yield* session.note(dbSubnetGroupArn ?? dbSubnetGroupName);
207 return {
208 dbSubnetGroupName: observed.DBSubnetGroupName,
209 dbSubnetGroupArn,
210 vpcId: observed.VpcId,
211 subnetIds: (observed.Subnets ?? []).flatMap((subnet) =>
212 subnet.SubnetIdentifier ? [subnet.SubnetIdentifier] : [],
213 ),
214 status: observed.SubnetGroupStatus,
215 supportedNetworkTypes: observed.SupportedNetworkTypes,
216 tags: desiredTags,
217 };
218 }),
219 list: () =>
220 // AWS account/region collection: `describeDBSubnetGroups` is
221 // paginated (items field `DBSubnetGroups`). Collect every page and
222 // map each group into the exact `read` Attributes shape. `read` does
223 // not hydrate tags from the cloud (it returns the cached
224 // `output.tags`), so we mirror that here with an empty tag map rather
225 // than issuing a per-item `listTagsForResource` call.
226 docdb.describeDBSubnetGroups.pages({}).pipe(
227 Stream.runCollect,
228 Effect.map((chunk) =>
229 Array.from(chunk).flatMap((page) =>
230 (page.DBSubnetGroups ?? []).flatMap((group) =>
231 group.DBSubnetGroupName
232 ? [
233 {
234 dbSubnetGroupName: group.DBSubnetGroupName,
235 dbSubnetGroupArn: group.DBSubnetGroupArn,
236 vpcId: group.VpcId,
237 subnetIds: (group.Subnets ?? []).flatMap((subnet) =>
238 subnet.SubnetIdentifier
239 ? [subnet.SubnetIdentifier]
240 : [],
241 ),
242 status: group.SubnetGroupStatus,
243 supportedNetworkTypes: group.SupportedNetworkTypes,
244 tags: {} as Record<string, string>,
245 },
246 ]
247 : [],
248 ),
249 ),
250 ),
251 ),
252 delete: Effect.fn(function* ({ output }) {
253 yield* docdb
254 .deleteDBSubnetGroup({
255 DBSubnetGroupName: output.dbSubnetGroupName,
256 })
257 .pipe(
258 Effect.catchTag("DBSubnetGroupNotFoundFault", () => Effect.void),
259 );
260 }),
261 };
262 }),
263 );