1import * as GCP from "@/GCP";
2import * as Output from "@/Output";
3import * as Test from "@/Test/Alchemy";
4import * as networkconnectivity from "@distilled.cloud/gcp/networkconnectivity_v1";
5import { expect } from "alchemy-test";
6import * as Effect from "effect/Effect";
7import { MinimumLogLevel } from "effect/References";
8import * as Schedule from "effect/Schedule";
9import { GcpEnvironment } from "@/GCP/Environment";
10
11const { test } = Test.make({ providers: GCP.providers() });
12
13const logLevel = Effect.provideService(
14 MinimumLogLevel,
15 process.env.DEBUG ? "Debug" : "Info",
16);
17
18// Gateway spokes provision managed routers and take well over 5 minutes.
19const runLifecycle = !!process.env.GCP_TEST_SLOW && !process.env.FAST;
20
21const waitUntilGone = (name: string) =>
22 networkconnectivity
23 .getProjectsLocationsSpokesGatewayAdvertisedRoutes({ name })
24 .pipe(
25 Effect.as("found" as const),
26 Effect.catchTag("NotFound", () => Effect.succeed("gone" as const)),
27 Effect.repeat({
28 schedule: Schedule.spaced("2 seconds"),
29 until: (status) => status === "gone",
30 times: 10,
31 }),
32 );
33
34test.provider(
35 "getProjectsLocationsSpokesGatewayAdvertisedRoutes on a missing route fails with a typed tag",
36 (stack) =>
37 Effect.gen(function* () {
38 const { project } = yield* GcpEnvironment.current;
39 yield* stack.destroy();
40
41 const error = yield* Effect.flip(
42 networkconnectivity.getProjectsLocationsSpokesGatewayAdvertisedRoutes({
43 name: `projects/${project}/locations/us-central1/spokes/alchemy-missing/gatewayAdvertisedRoutes/alchemy-missing`,
44 }),
45 );
46 expect(error._tag).toEqual("NotFound");
47
48 yield* stack.destroy();
49 }).pipe(logLevel),
50 {
51 tags: ["provider:gcp", "provider:gcp:networkconnectivity", "live"],
52 timeout: 90_000,
53 },
54);
55
56test.provider.skipIf(!runLifecycle)(
57 "create, update, and delete a gateway advertised route",
58 (stack) =>
59 Effect.gen(function* () {
60 yield* stack.destroy();
61
62 const created = yield* stack.deploy(
63 Effect.gen(function* () {
64 const hub = yield* GCP.NetworkConnectivity.Hub("Mesh", {
65 description: "ncc hub for gateway route",
66 policyMode: "PRESET",
67 presetTopology: "HYBRID_INSPECTION",
68 });
69 const spoke = yield* GCP.NetworkConnectivity.Spoke("Gw", {
70 location: "us-central1",
71 hub: hub.name,
72 group: Output.interpolate`${hub.name}/groups/gateways`,
73 gateway: {
74 capacity: "CAPACITY_1_GBPS",
75 ipRangeReservations: [{ ipRange: "10.200.0.0/23" }],
76 },
77 });
78 const route =
79 yield* GCP.NetworkConnectivity.SpokesGatewayAdvertisedRoute(
80 "OnPrem",
81 {
82 parent: spoke.name,
83 ipRange: "192.168.0.0/16",
84 recipient: "ADVERTISE_TO_HUB",
85 description: "route a",
86 labels: { env: "test" },
87 },
88 );
89 return { hub, spoke, route };
90 }),
91 );
92
93 expect(created.route.name).toContain("/gatewayAdvertisedRoutes/");
94 expect(created.route.parent).toEqual(created.spoke.name);
95 expect(created.route.ipRange).toEqual("192.168.0.0/16");
96 expect(created.route.labels).toMatchObject({ env: "test" });
97
98 const fetched =
99 yield* networkconnectivity.getProjectsLocationsSpokesGatewayAdvertisedRoutes(
100 { name: created.route.name },
101 );
102 expect(fetched.name).toEqual(created.route.name);
103 expect(fetched.ipRange).toEqual("192.168.0.0/16");
104 expect(fetched.labels?.env).toEqual("test");
105 expect(
106 Object.keys(fetched.labels ?? {}).some((key) =>
107 key.startsWith("alchemy-"),
108 ),
109 ).toEqual(true);
110
111 const updated = yield* stack.deploy(
112 Effect.gen(function* () {
113 const hub = yield* GCP.NetworkConnectivity.Hub("Mesh", {
114 hubId: created.hub.hubId,
115 description: "ncc hub for gateway route",
116 policyMode: "PRESET",
117 presetTopology: "HYBRID_INSPECTION",
118 });
119 const spoke = yield* GCP.NetworkConnectivity.Spoke("Gw", {
120 spokeId: created.spoke.spokeId,
121 location: "us-central1",
122 hub: hub.name,
123 group: Output.interpolate`${hub.name}/groups/gateways`,
124 gateway: {
125 capacity: "CAPACITY_1_GBPS",
126 ipRangeReservations: [{ ipRange: "10.200.0.0/23" }],
127 },
128 });
129 const route =
130 yield* GCP.NetworkConnectivity.SpokesGatewayAdvertisedRoute(
131 "OnPrem",
132 {
133 parent: spoke.name,
134 gatewayAdvertisedRouteId:
135 created.route.gatewayAdvertisedRouteId,
136 ipRange: "192.168.0.0/16",
137 recipient: "ADVERTISE_TO_HUB",
138 priority: 200,
139 description: "route a",
140 labels: { env: "test" },
141 },
142 );
143 return { hub, spoke, route };
144 }),
145 );
146
147 // Priority updates in place.
148 expect(updated.route.name).toEqual(created.route.name);
149 expect(updated.route.uniqueId).toEqual(created.route.uniqueId);
150 expect(updated.route.description).toEqual("route a");
151 expect(updated.route.priority).toEqual(200);
152
153 const replaced = yield* stack.deploy(
154 Effect.gen(function* () {
155 const hub = yield* GCP.NetworkConnectivity.Hub("Mesh", {
156 hubId: created.hub.hubId,
157 description: "ncc hub for gateway route",
158 policyMode: "PRESET",
159 presetTopology: "HYBRID_INSPECTION",
160 });
161 const spoke = yield* GCP.NetworkConnectivity.Spoke("Gw", {
162 spokeId: created.spoke.spokeId,
163 location: "us-central1",
164 hub: hub.name,
165 group: Output.interpolate`${hub.name}/groups/gateways`,
166 gateway: {
167 capacity: "CAPACITY_1_GBPS",
168 ipRangeReservations: [{ ipRange: "10.200.0.0/23" }],
169 },
170 });
171 const route =
172 yield* GCP.NetworkConnectivity.SpokesGatewayAdvertisedRoute(
173 "OnPrem",
174 {
175 parent: spoke.name,
176 gatewayAdvertisedRouteId:
177 created.route.gatewayAdvertisedRouteId,
178 ipRange: "192.168.0.0/16",
179 recipient: "ADVERTISE_TO_HUB",
180 priority: 200,
181 description: "route b",
182 labels: { env: "prod", role: "gw" },
183 },
184 );
185 return { hub, spoke, route };
186 }),
187 );
188
189 // Labels are immutable: changing them replaces the route (the pinned
190 // id is reused, so the old route is deleted first).
191 expect(replaced.route.name).toEqual(created.route.name);
192 expect(replaced.route.uniqueId).not.toEqual(created.route.uniqueId);
193 expect(replaced.route.labels).toMatchObject({ env: "prod", role: "gw" });
194
195 const refetched =
196 yield* networkconnectivity.getProjectsLocationsSpokesGatewayAdvertisedRoutes(
197 { name: replaced.route.name },
198 );
199 expect(refetched.labels?.env).toEqual("prod");
200 expect(refetched.labels?.role).toEqual("gw");
201 expect(refetched.priority).toEqual(200);
202
203 yield* stack.destroy();
204
205 const gone = yield* waitUntilGone(created.route.name);
206 expect(gone).toEqual("gone");
207 }).pipe(logLevel),
208 {
209 tags: ["provider:gcp", "provider:gcp:networkconnectivity", "live"],
210 timeout: 1_800_000,
211 retry: 0,
212 },
213);