1import { GcpEnvironment } from "@/GCP/Environment";
2import * as GCP from "@/GCP";
3import * as Test from "@/Test/Alchemy";
4import * as resourcemanager from "@distilled.cloud/gcp/cloudresourcemanager_v3";
5import * as servicenetworking from "@distilled.cloud/gcp/servicenetworking_v1";
6import { expect } from "alchemy-test";
7import * as Effect from "effect/Effect";
8import { MinimumLogLevel } from "effect/References";
9import * as Schedule from "effect/Schedule";
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
19const parent = "services/servicenetworking.googleapis.com";
20
21const waitUntilGone = (consumerNetwork: string) =>
22 servicenetworking
23 .listServicesConnections({
24 parent,
25 network: consumerNetwork,
26 })
27 .pipe(
28 Effect.map((page) =>
29 (page.connections ?? []).length === 0
30 ? ("gone" as const)
31 : ("found" as const),
32 ),
33 Effect.catchTag("NotFound", () => Effect.succeed("gone" as const)),
34 Effect.repeat({
35 schedule: Schedule.spaced("2 seconds"),
36 until: (status) => status === "gone",
37 times: 15,
38 }),
39 );
40
41test.provider(
42 "listServicesConnections on a missing network returns no connections",
43 (stack) =>
44 Effect.gen(function* () {
45 const { project } = yield* GcpEnvironment.current;
46
47 yield* stack.destroy();
48
49 const resource = yield* resourcemanager.getProjects({
50 name: `projects/${project}`,
51 });
52 const projectNumber = resource.name?.split("/").pop() ?? project;
53 const page = yield* servicenetworking.listServicesConnections({
54 parent,
55 network: `projects/${projectNumber}/global/networks/alchemy-sn-missing`,
56 });
57 expect(page.connections ?? []).toEqual([]);
58
59 yield* stack.destroy();
60 }).pipe(logLevel),
61 {
62 tags: ["provider:gcp", "provider:gcp:servicenetworking", "live"],
63 timeout: 90_000,
64 },
65);
66
67test.provider.skipIf(!!process.env.FAST)(
68 "create, update, and delete a service networking connection",
69 (stack) =>
70 Effect.gen(function* () {
71 const { project } = yield* GcpEnvironment.current;
72
73 yield* stack.destroy();
74
75 const created = yield* stack.deploy(
76 Effect.gen(function* () {
77 const network = yield* GCP.Compute.Network("Vpc", {
78 autoCreateSubnetworks: false,
79 });
80 const range = yield* GCP.Compute.GlobalAddress("PsaRange", {
81 addressType: "INTERNAL",
82 purpose: "VPC_PEERING",
83 network: network.selfLink,
84 prefixLength: 24,
85 });
86 const extra = yield* GCP.Compute.GlobalAddress("PsaExtra", {
87 addressType: "INTERNAL",
88 purpose: "VPC_PEERING",
89 network: network.selfLink,
90 prefixLength: 24,
91 });
92 const connection = yield* GCP.ServiceNetworking.Connection("Psa", {
93 network: network.networkName,
94 reservedPeeringRanges: [range.addressName],
95 });
96 return { network, range, extra, connection };
97 }),
98 );
99
100 expect(created.connection.networkName).toEqual(
101 created.network.networkName,
102 );
103 expect(created.connection.service).toEqual(parent);
104 expect(created.connection.peering).toEqual(
105 "servicenetworking-googleapis-com",
106 );
107 expect(created.connection.reservedPeeringRanges).toEqual([
108 created.range.addressName,
109 ]);
110 expect(created.connection.project).toEqual(project);
111 expect(created.connection.projectNumber).toEqual(expect.any(String));
112
113 const listed = yield* servicenetworking.listServicesConnections({
114 parent,
115 network: created.connection.network,
116 });
117 const fetched = (listed.connections ?? []).find(
118 (connection) =>
119 (connection.network ?? "") === created.connection.network,
120 );
121 expect(fetched?.peering).toEqual(created.connection.peering);
122 expect(fetched?.reservedPeeringRanges).toEqual([
123 created.range.addressName,
124 ]);
125
126 const updated = yield* stack.deploy(
127 Effect.gen(function* () {
128 const network = yield* GCP.Compute.Network("Vpc", {
129 networkName: created.network.networkName,
130 autoCreateSubnetworks: false,
131 });
132 const range = yield* GCP.Compute.GlobalAddress("PsaRange", {
133 addressName: created.range.addressName,
134 addressType: "INTERNAL",
135 purpose: "VPC_PEERING",
136 network: network.selfLink,
137 prefixLength: 24,
138 });
139 const extra = yield* GCP.Compute.GlobalAddress("PsaExtra", {
140 addressName: created.extra.addressName,
141 addressType: "INTERNAL",
142 purpose: "VPC_PEERING",
143 network: network.selfLink,
144 prefixLength: 24,
145 });
146 const connection = yield* GCP.ServiceNetworking.Connection("Psa", {
147 network: network.networkName,
148 reservedPeeringRanges: [range.addressName, extra.addressName],
149 });
150 return { network, range, extra, connection };
151 }),
152 );
153
154 expect(updated.connection.network).toEqual(created.connection.network);
155 expect(updated.connection.peering).toEqual(created.connection.peering);
156 expect([...updated.connection.reservedPeeringRanges].sort()).toEqual(
157 [created.range.addressName, created.extra.addressName].sort(),
158 );
159
160 const listedUpdated = yield* servicenetworking.listServicesConnections({
161 parent,
162 network: updated.connection.network,
163 });
164 const fetchedUpdated = (listedUpdated.connections ?? [])[0];
165 expect([...(fetchedUpdated?.reservedPeeringRanges ?? [])].sort()).toEqual(
166 [created.range.addressName, created.extra.addressName].sort(),
167 );
168
169 yield* stack.destroy();
170
171 const gone = yield* waitUntilGone(created.connection.network);
172 expect(gone).toEqual("gone");
173 }).pipe(logLevel, withNetworkSlot),
174 {
175 tags: ["provider:gcp", "provider:gcp:servicenetworking", "live"],
176 timeout: 600_000,
177 },
178);