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 { GcpEnvironment } from "@/GCP/Environment";
9
10const { test } = Test.make({ providers: GCP.providers() });
11
12const logLevel = Effect.provideService(
13 MinimumLogLevel,
14 process.env.DEBUG ? "Debug" : "Info",
15);
16
17const region = "us-central1";
18const poolName = "alchemy-fr-test-pool";
19
20const waitUntilGone = (forwardingRuleName: string) =>
21 GcpEnvironment.current.pipe(
22 Effect.flatMap(({ project }) =>
23 compute
24 .getForwardingRules({
25 project,
26 region,
27 forwardingRule: forwardingRuleName,
28 })
29 .pipe(
30 Effect.as("found" as const),
31 Effect.catchTag("NotFound", () => Effect.succeed("gone" as const)),
32 Effect.repeat({
33 schedule: Schedule.spaced("2 seconds"),
34 until: (status) => status === "gone",
35 times: 10,
36 }),
37 ),
38 ),
39 );
40
41const waitOp = (operation: compute.Operation) => {
42 if (operation.status === "DONE") return Effect.succeed(operation);
43 const name = (operation.name ?? "").split("/").pop() ?? "";
44 return GcpEnvironment.current.pipe(
45 Effect.flatMap(({ project }) =>
46 compute.getRegionOperations({ project, region, operation: name }).pipe(
47 Effect.repeat({
48 schedule: Schedule.spaced("2 seconds"),
49 until: (op) => op.status === "DONE",
50 times: 12,
51 }),
52 ),
53 ),
54 );
55};
56
57const ensureTargetPool = () =>
58 Effect.gen(function* () {
59 const { project } = yield* GcpEnvironment.current;
60 const existing = yield* compute
61 .getTargetPools({ project, region, targetPool: poolName })
62 .pipe(Effect.catchTag("NotFound", () => Effect.succeed(undefined)));
63 if (existing !== undefined) return existing;
64 yield* compute
65 .insertTargetPools({
66 project,
67 region,
68 body: { name: poolName, description: "alchemy forwarding-rule test" },
69 })
70 .pipe(
71 Effect.flatMap(waitOp),
72 Effect.catchTag("Conflict", () => Effect.void),
73 );
74 return yield* compute.getTargetPools({
75 project,
76 region,
77 targetPool: poolName,
78 });
79 });
80
81const deleteTargetPool = () =>
82 GcpEnvironment.current.pipe(
83 Effect.flatMap(({ project }) =>
84 compute.deleteTargetPools({ project, region, targetPool: poolName }).pipe(
85 Effect.flatMap(waitOp),
86 Effect.catchTag("NotFound", () => Effect.void),
87 Effect.catchTag("Conflict", () => Effect.void),
88 ),
89 ),
90 );
91
92test.provider(
93 "create, update, and delete a regional forwarding rule",
94 (stack) =>
95 Effect.gen(function* () {
96 yield* stack.destroy();
97 const pool = yield* ensureTargetPool();
98 expect(pool.selfLink).toEqual(expect.any(String));
99
100 const created = yield* stack.deploy(
101 Effect.gen(function* () {
102 return yield* GCP.Compute.ForwardingRule("Frontend", {
103 region,
104 target: pool.selfLink,
105 ipProtocol: "TCP",
106 portRange: "80",
107 labels: { env: "test" },
108 });
109 }),
110 );
111
112 expect(created.forwardingRuleName).toEqual(expect.any(String));
113 expect(created.region).toEqual(region);
114 expect(created.ipAddress).toEqual(expect.any(String));
115 expect(created.ipProtocol).toEqual("TCP");
116 expect(created.loadBalancingScheme).toEqual("EXTERNAL");
117 expect(created.labels).toMatchObject({ env: "test" });
118 expect(created.target).toEqual(expect.stringContaining(poolName));
119
120 const fetched = yield* compute.getForwardingRules({
121 project: created.project,
122 region: created.region,
123 forwardingRule: created.forwardingRuleName,
124 });
125 expect(fetched.name).toEqual(created.forwardingRuleName);
126 expect(fetched.IPAddress).toEqual(created.ipAddress);
127 expect(fetched.IPProtocol).toEqual("TCP");
128 expect(fetched.labels?.env).toEqual("test");
129 expect(fetched.labels?.["alchemy-id"]).toEqual(expect.any(String));
130 expect(fetched.target).toEqual(expect.stringContaining(poolName));
131
132 const updated = yield* stack.deploy(
133 Effect.gen(function* () {
134 return yield* GCP.Compute.ForwardingRule("Frontend", {
135 forwardingRuleName: created.forwardingRuleName,
136 region,
137 target: pool.selfLink,
138 ipProtocol: "TCP",
139 portRange: "80",
140 labels: { env: "prod", role: "frontend" },
141 });
142 }),
143 );
144
145 expect(updated.forwardingRuleName).toEqual(created.forwardingRuleName);
146 expect(updated.ipAddress).toEqual(created.ipAddress);
147 expect(updated.labels).toMatchObject({ env: "prod", role: "frontend" });
148
149 const fetchedUpdated = yield* compute.getForwardingRules({
150 project: updated.project,
151 region: updated.region,
152 forwardingRule: updated.forwardingRuleName,
153 });
154 expect(fetchedUpdated.labels?.env).toEqual("prod");
155 expect(fetchedUpdated.labels?.role).toEqual("frontend");
156
157 yield* stack.destroy();
158
159 const gone = yield* waitUntilGone(created.forwardingRuleName);
160 expect(gone).toEqual("gone");
161 }).pipe(logLevel, Effect.ensuring(deleteTargetPool().pipe(Effect.ignore))),
162 { tags: ["provider:gcp", "provider:gcp:compute", "live"], timeout: 90_000 },
163);