1import * as GCP from "@/GCP";
2import * as Test from "@/Test/Alchemy";
3import * as compute from "@distilled.cloud/gcp/compute_v1";
4import { expect } from "alchemy-test";
5import * as Effect from "effect/Effect";
6import { MinimumLogLevel } from "effect/References";
7import * as Schedule from "effect/Schedule";
8import { DEFAULT_NETWORK } from "../networkQuota.ts";
9
10const { test } = Test.make({ providers: GCP.providers() });
11
12const logLevel = Effect.provideService(
13 MinimumLogLevel,
14 process.env.DEBUG ? "Debug" : "Info",
15);
16
17const SHARED_SECRET = "alchemy-test-shared-secret";
18
19const waitUntilGone = (
20 project: string,
21 region: string,
22 vpnTunnelName: string,
23) =>
24 compute.getVpnTunnels({ project, region, vpnTunnel: vpnTunnelName }).pipe(
25 Effect.as("found" as const),
26 Effect.catchTag("NotFound", () => Effect.succeed("gone" as const)),
27 Effect.repeat({
28 schedule: Schedule.spaced("1 second"),
29 until: (status) => status === "gone",
30 times: 15,
31 }),
32 );
33
34test.provider(
35 "create, update, and delete an HA VPN tunnel",
36 (stack) =>
37 Effect.gen(function* () {
38 yield* stack.destroy();
39
40 const created = yield* stack.deploy(
41 Effect.gen(function* () {
42 const gateway = yield* GCP.Compute.VpnGateway("Gateway", {
43 region: "us-central1",
44 network: DEFAULT_NETWORK,
45 });
46 const router = yield* GCP.Compute.Router("Edge", {
47 region: "us-central1",
48 network: DEFAULT_NETWORK,
49 bgp: { asn: 64514 },
50 });
51 const peer = yield* GCP.Compute.ExternalVpnGateway("Peer", {
52 redundancyType: "SINGLE_IP_INTERNALLY_REDUNDANT",
53 interfaces: [{ id: 0, ipAddress: "15.0.0.120" }],
54 });
55 const tunnel = yield* GCP.Compute.VpnTunnel("Tunnel", {
56 region: "us-central1",
57 vpnGateway: gateway.vpnGatewayName,
58 vpnGatewayInterface: 0,
59 peerExternalGateway: peer.externalVpnGatewayName,
60 peerExternalGatewayInterface: 0,
61 router: router.routerName,
62 sharedSecret: SHARED_SECRET,
63 description: "ha vpn tunnel",
64 labels: { env: "test" },
65 });
66 return { gateway, router, peer, tunnel };
67 }),
68 );
69
70 expect(created.tunnel.vpnTunnelName).toEqual(expect.any(String));
71 expect(created.tunnel.region).toEqual("us-central1");
72 expect(created.tunnel.description).toEqual("ha vpn tunnel");
73 expect(created.tunnel.labels).toMatchObject({ env: "test" });
74 expect(created.tunnel.vpnGateway).toEqual(
75 expect.stringContaining(created.gateway.vpnGatewayName),
76 );
77 expect(created.tunnel.vpnGatewayInterface).toEqual(0);
78 expect(created.tunnel.peerExternalGateway).toEqual(
79 expect.stringContaining(created.peer.externalVpnGatewayName),
80 );
81 expect(created.tunnel.peerExternalGatewayInterface).toEqual(0);
82 expect(created.tunnel.router).toEqual(
83 expect.stringContaining(created.router.routerName),
84 );
85 expect(created.tunnel.ikeVersion).toEqual(2);
86 expect(created.tunnel.sharedSecretHash).toEqual(expect.any(String));
87
88 const fetched = yield* compute.getVpnTunnels({
89 project: created.tunnel.project,
90 region: created.tunnel.region,
91 vpnTunnel: created.tunnel.vpnTunnelName,
92 });
93 expect(fetched.name).toEqual(created.tunnel.vpnTunnelName);
94 expect(fetched.description).toEqual("ha vpn tunnel");
95 expect(fetched.labels?.env).toEqual("test");
96 expect(fetched.labels?.["alchemy-id"]).toEqual(expect.any(String));
97 expect(fetched.vpnGateway).toEqual(
98 expect.stringContaining(created.gateway.vpnGatewayName),
99 );
100 expect(fetched.vpnGatewayInterface).toEqual(0);
101 expect(fetched.peerExternalGateway).toEqual(
102 expect.stringContaining(created.peer.externalVpnGatewayName),
103 );
104 expect(fetched.router).toEqual(
105 expect.stringContaining(created.router.routerName),
106 );
107 expect(fetched.ikeVersion).toEqual(2);
108
109 const updated = yield* stack.deploy(
110 Effect.gen(function* () {
111 const gateway = yield* GCP.Compute.VpnGateway("Gateway", {
112 vpnGatewayName: created.gateway.vpnGatewayName,
113 region: "us-central1",
114 network: DEFAULT_NETWORK,
115 });
116 const router = yield* GCP.Compute.Router("Edge", {
117 routerName: created.router.routerName,
118 region: "us-central1",
119 network: DEFAULT_NETWORK,
120 bgp: { asn: 64514 },
121 });
122 const peer = yield* GCP.Compute.ExternalVpnGateway("Peer", {
123 externalVpnGatewayName: created.peer.externalVpnGatewayName,
124 redundancyType: "SINGLE_IP_INTERNALLY_REDUNDANT",
125 interfaces: [{ id: 0, ipAddress: "15.0.0.120" }],
126 });
127 const tunnel = yield* GCP.Compute.VpnTunnel("Tunnel", {
128 vpnTunnelName: created.tunnel.vpnTunnelName,
129 region: "us-central1",
130 vpnGateway: gateway.vpnGatewayName,
131 vpnGatewayInterface: 0,
132 peerExternalGateway: peer.externalVpnGatewayName,
133 peerExternalGatewayInterface: 0,
134 router: router.routerName,
135 sharedSecret: SHARED_SECRET,
136 description: "ha vpn tunnel",
137 labels: { env: "prod", role: "vpn" },
138 });
139 return { gateway, router, peer, tunnel };
140 }),
141 );
142
143 expect(updated.tunnel.vpnTunnelName).toEqual(
144 created.tunnel.vpnTunnelName,
145 );
146 expect(updated.tunnel.vpnTunnelId).toEqual(created.tunnel.vpnTunnelId);
147 expect(updated.tunnel.labels).toMatchObject({
148 env: "prod",
149 role: "vpn",
150 });
151
152 const fetchedUpdated = yield* compute.getVpnTunnels({
153 project: updated.tunnel.project,
154 region: updated.tunnel.region,
155 vpnTunnel: updated.tunnel.vpnTunnelName,
156 });
157 expect(fetchedUpdated.labels?.env).toEqual("prod");
158 expect(fetchedUpdated.labels?.role).toEqual("vpn");
159 expect(fetchedUpdated.id).toEqual(created.tunnel.vpnTunnelId);
160
161 yield* stack.destroy();
162
163 const gone = yield* waitUntilGone(
164 created.tunnel.project,
165 created.tunnel.region,
166 created.tunnel.vpnTunnelName,
167 );
168 expect(gone).toEqual("gone");
169 }).pipe(logLevel),
170 { tags: ["provider:gcp", "provider:gcp:compute", "live"], timeout: 240_000 },
171);