1import * as EC2 from "@distilled.cloud/aws/ec2";
2import * as r53r from "@distilled.cloud/aws/route53resolver";
3import * as Effect from "effect/Effect";
4import * as Schedule from "effect/Schedule";
5import { getDefaultVpc } from "../DefaultVpc.ts";
6
7/**
8 * Resolve the default VPC, two default-for-AZ subnets, and the default
9 * security group — the network a resolver endpoint's interfaces land in.
10 */
11export const defaultNetwork = Effect.gen(function* () {
12 const vpc = yield* getDefaultVpc;
13 const subnets = yield* EC2.describeSubnets({
14 Filters: [
15 { Name: "vpc-id", Values: [vpc.vpcId] },
16 { Name: "default-for-az", Values: ["true"] },
17 ],
18 });
19 const subnetIds = (subnets.Subnets ?? [])
20 .map((s) => s.SubnetId)
21 .filter((id): id is string => id !== undefined)
22 .sort()
23 .slice(0, 2);
24 if (subnetIds.length < 2) {
25 return yield* Effect.die(
26 new Error("default VPC has fewer than 2 default-for-AZ subnets"),
27 );
28 }
29 const groups = yield* EC2.describeSecurityGroups({
30 Filters: [
31 { Name: "vpc-id", Values: [vpc.vpcId] },
32 { Name: "group-name", Values: ["default"] },
33 ],
34 });
35 const securityGroupId = groups.SecurityGroups?.[0]?.GroupId;
36 if (securityGroupId === undefined) {
37 return yield* Effect.die(
38 new Error("default VPC has no default security group"),
39 );
40 }
41 return { vpcId: vpc.vpcId as string, subnetIds, securityGroupId };
42}).pipe(Effect.orDie);
43
44/**
45 * Endpoint deletion is asynchronous — accept `DELETING` (deletion
46 * initiated) or fully gone, retrying briefly.
47 */
48export const assertEndpointDeleting = (endpointId: string) =>
49 r53r.getResolverEndpoint({ ResolverEndpointId: endpointId }).pipe(
50 Effect.flatMap((r) =>
51 r.ResolverEndpoint?.Status === "DELETING"
52 ? Effect.void
53 : Effect.fail(
54 new Error(
55 `resolver endpoint '${endpointId}' still ${r.ResolverEndpoint?.Status}`,
56 ),
57 ),
58 ),
59 Effect.catchTag("ResourceNotFoundException", () => Effect.void),
60 Effect.retry({
61 while: (e) => e instanceof Error,
62 schedule: Schedule.max([
63 Schedule.fixed("2 seconds"),
64 Schedule.recurs(10),
65 ]),
66 }),
67 );
68
69/**
70 * Rule deletion is asynchronous — accept `DELETING` or fully gone,
71 * retrying briefly.
72 */
73export const assertRuleGone = (ruleId: string) =>
74 r53r.getResolverRule({ ResolverRuleId: ruleId }).pipe(
75 Effect.flatMap((r) =>
76 r.ResolverRule?.Status === "DELETING"
77 ? Effect.void
78 : Effect.fail(
79 new Error(
80 `resolver rule '${ruleId}' still ${r.ResolverRule?.Status}`,
81 ),
82 ),
83 ),
84 Effect.catchTag("ResourceNotFoundException", () => Effect.void),
85 Effect.retry({
86 while: (e) => e instanceof Error,
87 schedule: Schedule.max([
88 Schedule.fixed("2 seconds"),
89 Schedule.recurs(10),
90 ]),
91 }),
92 );