1import * as GCP from "@/GCP";
2import * as Test from "@/Test/Alchemy";
3import * as networkconnectivity from "@distilled.cloud/gcp/networkconnectivity_v1";
4import { expect } from "alchemy-test";
5import * as Effect from "effect/Effect";
6import { MinimumLogLevel } from "effect/References";
7import * as Result from "effect/Result";
8import * as Schedule from "effect/Schedule";
9import { GcpEnvironment } from "@/GCP/Environment";
10import { withNetworkSlot } from "../networkQuota.ts";
11
12const { test } = Test.make({ providers: GCP.providers() });
13
14const logLevel = Effect.provideService(
15 MinimumLogLevel,
16 process.env.DEBUG ? "Debug" : "Info",
17);
18
19// Transports are Cross-Cloud Interconnect to a partner cloud: they need an
20// allowlisted remote profile and a real peer account (the probe asserts the
21// NotFound for a missing profile). Set GCP_TEST_TRANSPORT=1 on an entitled
22// project.
23const runLifecycle = !!process.env.GCP_TEST_TRANSPORT;
24
25const location = "us-east4";
26const remoteProfile = "aws-us-east-1";
27
28const transportName = (project: string, id: string) =>
29 `projects/${project}/locations/${location}/transports/${id}`;
30
31const waitUntilGone = (name: string) =>
32 networkconnectivity.getProjectsLocationsTransports({ name }).pipe(
33 Effect.as("found" as const),
34 Effect.catchTag("NotFound", () => Effect.succeed("gone" as const)),
35 Effect.repeat({
36 schedule: Schedule.spaced("2 seconds"),
37 until: (status) => status === "gone",
38 times: 10,
39 }),
40 );
41
42const probeCreate = (project: string, transportId: string, profileId: string) =>
43 networkconnectivity
44 .createProjectsLocationsTransports({
45 parent: `projects/${project}/locations/${location}`,
46 transportId,
47 body: {
48 network: `projects/${project}/global/networks/default`,
49 remoteProfile: `projects/${project}/locations/${location}/remoteTransportProfiles/${profileId}`,
50 bandwidth: "BPS_1G",
51 remoteAccountId: "123456789012",
52 description: "alchemy transport probe",
53 },
54 })
55 .pipe(Effect.result);
56
57test.provider(
58 "getProjectsLocationsTransports on a missing transport fails with a typed tag",
59 (stack) =>
60 Effect.gen(function* () {
61 const { project } = yield* GcpEnvironment.current;
62 yield* stack.destroy();
63
64 const error = yield* Effect.flip(
65 networkconnectivity.getProjectsLocationsTransports({
66 name: transportName(project, "alchemy-tp-missing"),
67 }),
68 );
69 expect(error._tag).toEqual("NotFound");
70
71 const page = yield* networkconnectivity.listProjectsLocationsTransports({
72 parent: `projects/${project}/locations/${location}`,
73 pageSize: 10,
74 });
75 expect(
76 (page.transports ?? []).map((transport) => transport.name),
77 ).not.toContain(transportName(project, "alchemy-tp-missing"));
78
79 yield* stack.destroy();
80 }).pipe(logLevel),
81 {
82 tags: ["provider:gcp", "provider:gcp:networkconnectivity", "live"],
83 timeout: 90_000,
84 },
85);
86
87test.provider(
88 "createProjectsLocationsTransports with a missing remote profile fails with NotFound",
89 (stack) =>
90 Effect.gen(function* () {
91 const { project } = yield* GcpEnvironment.current;
92 yield* stack.destroy();
93
94 const result = yield* probeCreate(
95 project,
96 "alchemy-tp-probe",
97 "aws-profile-missing",
98 );
99 expect(Result.isFailure(result)).toBe(true);
100 if (Result.isFailure(result)) {
101 expect(result.failure._tag).toBe("NotFound");
102 expect(result.failure.message ?? "").toContain(
103 "remoteTransportProfiles/aws-profile-missing",
104 );
105 }
106
107 yield* stack.destroy();
108 }).pipe(logLevel),
109 {
110 tags: ["provider:gcp", "provider:gcp:networkconnectivity", "live"],
111 timeout: 90_000,
112 },
113);
114
115test.provider.skipIf(!runLifecycle)(
116 "create, update, and delete a transport",
117 (stack) =>
118 Effect.gen(function* () {
119 yield* stack.destroy();
120
121 const created = yield* stack.deploy(
122 Effect.gen(function* () {
123 const network = yield* GCP.Compute.Network("TransportVpc", {
124 autoCreateSubnetworks: false,
125 });
126 const transport = yield* GCP.NetworkConnectivity.Transport("Aws", {
127 location,
128 network: network.selfLink.as<string>(),
129 remoteProfile,
130 bandwidth: "BPS_1G",
131 remoteAccountId: "123456789012",
132 advertisedRoutes: ["10.0.0.0/8"],
133 description: "transport a",
134 labels: { env: "test" },
135 });
136 return { network, transport };
137 }),
138 );
139
140 expect(created.transport.name).toContain("/transports/");
141 expect(created.transport.name).toContain(`/locations/${location}/`);
142 expect(created.transport.transportId).toEqual(expect.any(String));
143 expect(created.transport.location).toEqual(location);
144 expect(created.transport.networkName).toEqual(
145 created.network.networkName,
146 );
147 expect(created.transport.remoteProfileId).toEqual(remoteProfile);
148 expect(created.transport.bandwidth).toEqual("BPS_1G");
149 expect(created.transport.description).toEqual("transport a");
150 expect(created.transport.labels).toMatchObject({ env: "test" });
151 expect(created.transport.createTime).toEqual(expect.any(String));
152 expect(["PENDING_KEY", "PENDING_CONFIG", "ACTIVE", "CREATING"]).toContain(
153 created.transport.state,
154 );
155
156 const fetched = yield* networkconnectivity.getProjectsLocationsTransports(
157 {
158 name: created.transport.name,
159 },
160 );
161 expect(fetched.name).toEqual(created.transport.name);
162 expect(fetched.labels?.env).toEqual("test");
163 expect(fetched.description).toEqual("transport a");
164 expect(
165 Object.keys(fetched.labels ?? {}).some((key) =>
166 key.startsWith("alchemy-"),
167 ),
168 ).toEqual(true);
169
170 const updated = yield* stack.deploy(
171 Effect.gen(function* () {
172 const network = yield* GCP.Compute.Network("TransportVpc", {
173 networkName: created.network.networkName,
174 autoCreateSubnetworks: false,
175 });
176 const transport = yield* GCP.NetworkConnectivity.Transport("Aws", {
177 transportId: created.transport.transportId,
178 location,
179 network: network.selfLink.as<string>(),
180 remoteProfile,
181 bandwidth: "BPS_1G",
182 remoteAccountId: "123456789012",
183 advertisedRoutes: ["10.0.0.0/8"],
184 description: "transport b",
185 labels: { env: "prod", role: "cci" },
186 });
187 return { network, transport };
188 }),
189 );
190
191 expect(updated.transport.name).toEqual(created.transport.name);
192 expect(updated.transport.transportId).toEqual(
193 created.transport.transportId,
194 );
195 expect(updated.transport.description).toEqual("transport b");
196 expect(updated.transport.labels).toMatchObject({
197 env: "prod",
198 role: "cci",
199 });
200
201 const refetched =
202 yield* networkconnectivity.getProjectsLocationsTransports({
203 name: created.transport.name,
204 });
205 expect(refetched.description).toEqual("transport b");
206 expect(refetched.labels?.env).toEqual("prod");
207 expect(refetched.labels?.role).toEqual("cci");
208
209 yield* stack.destroy();
210
211 const gone = yield* waitUntilGone(created.transport.name);
212 expect(gone).toEqual("gone");
213 }).pipe(logLevel, withNetworkSlot),
214 {
215 tags: ["provider:gcp", "provider:gcp:networkconnectivity", "live"],
216 timeout: 120_000,
217 },
218);