1/**
2 * Typed out-of-band "assert gone" helpers shared by the EC2 test suites.
3 *
4 * Each helper describes the resource by id after the final `stack.destroy()`
5 * and succeeds only when the API returns the typed NotFound tag (or, for
6 * instances, a terminal state). A bounded retry absorbs EC2's eventual
7 * consistency; anything else fails the test — proving the suite left zero
8 * orphans.
9 */
10import * as ec2 from "@distilled.cloud/aws/ec2";
11import * as Data from "effect/Data";
12import * as Effect from "effect/Effect";
13import * as Schedule from "effect/Schedule";
14
15class ResourceStillExists extends Data.TaggedError("ResourceStillExists")<{
16 readonly id: string;
17}> {}
18
19const goneRetry = {
20 while: (e: { _tag: string }): boolean => e._tag === "ResourceStillExists",
21 schedule: Schedule.spaced("3 seconds"),
22 times: 10,
23} as const;
24
25export const assertVpcGone = Effect.fn(function* (vpcId: string) {
26 yield* ec2.describeVpcs({ VpcIds: [vpcId] }).pipe(
27 Effect.flatMap(() => Effect.fail(new ResourceStillExists({ id: vpcId }))),
28 Effect.retry(goneRetry),
29 Effect.catchTag("InvalidVpcID.NotFound", () => Effect.void),
30 );
31});
32
33export const assertEipGone = Effect.fn(function* (allocationId: string) {
34 yield* ec2.describeAddresses({ AllocationIds: [allocationId] }).pipe(
35 Effect.flatMap(() =>
36 Effect.fail(new ResourceStillExists({ id: allocationId })),
37 ),
38 Effect.retry(goneRetry),
39 Effect.catchTag("InvalidAllocationID.NotFound", () => Effect.void),
40 );
41});
42
43export const assertInternetGatewayGone = Effect.fn(function* (igwId: string) {
44 yield* ec2.describeInternetGateways({ InternetGatewayIds: [igwId] }).pipe(
45 Effect.flatMap(() => Effect.fail(new ResourceStillExists({ id: igwId }))),
46 Effect.retry(goneRetry),
47 Effect.catchTag("InvalidInternetGatewayID.NotFound", () => Effect.void),
48 );
49});
50
51export const assertRouteTableGone = Effect.fn(function* (routeTableId: string) {
52 yield* ec2.describeRouteTables({ RouteTableIds: [routeTableId] }).pipe(
53 Effect.flatMap(() =>
54 Effect.fail(new ResourceStillExists({ id: routeTableId })),
55 ),
56 Effect.retry(goneRetry),
57 Effect.catchTag("InvalidRouteTableID.NotFound", () => Effect.void),
58 );
59});
60
61export const assertSecurityGroupGone = Effect.fn(function* (groupId: string) {
62 yield* ec2.describeSecurityGroups({ GroupIds: [groupId] }).pipe(
63 Effect.flatMap(() => Effect.fail(new ResourceStillExists({ id: groupId }))),
64 Effect.retry(goneRetry),
65 Effect.catchTag("InvalidGroup.NotFound", () => Effect.void),
66 );
67});
68
69export const assertNetworkAclGone = Effect.fn(function* (networkAclId: string) {
70 yield* ec2.describeNetworkAcls({ NetworkAclIds: [networkAclId] }).pipe(
71 Effect.flatMap(() =>
72 Effect.fail(new ResourceStillExists({ id: networkAclId })),
73 ),
74 Effect.retry(goneRetry),
75 Effect.catchTag("InvalidNetworkAclID.NotFound", () => Effect.void),
76 );
77});
78
79export const assertSubnetGone = Effect.fn(function* (subnetId: string) {
80 yield* ec2.describeSubnets({ SubnetIds: [subnetId] }).pipe(
81 Effect.flatMap(() =>
82 Effect.fail(new ResourceStillExists({ id: subnetId })),
83 ),
84 Effect.retry(goneRetry),
85 Effect.catchTag("InvalidSubnetID.NotFound", () => Effect.void),
86 );
87});
88
89export const assertKeyPairGone = Effect.fn(function* (keyPairId: string) {
90 yield* ec2.describeKeyPairs({ KeyPairIds: [keyPairId] }).pipe(
91 Effect.flatMap(() =>
92 Effect.fail(new ResourceStillExists({ id: keyPairId })),
93 ),
94 Effect.retry(goneRetry),
95 Effect.catchTag("InvalidKeyPair.NotFound", () => Effect.void),
96 );
97});
98
99/**
100 * A terminated instance remains visible in `describeInstances` for a while;
101 * `terminated` (or the id no longer resolving) is the terminal, non-billing
102 * state that proves the suite left nothing running.
103 */
104export const assertInstanceTerminated = Effect.fn(function* (
105 instanceId: string,
106) {
107 yield* ec2.describeInstances({ InstanceIds: [instanceId] }).pipe(
108 Effect.flatMap((result) => {
109 const state = result.Reservations?.[0]?.Instances?.[0]?.State?.Name;
110 return state === undefined || state === "terminated"
111 ? Effect.void
112 : Effect.fail(new ResourceStillExists({ id: instanceId }));
113 }),
114 Effect.retry(goneRetry),
115 Effect.catchTag("InvalidInstanceID.NotFound", () => Effect.void),
116 );
117});