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";
18
19const waitUntilGone = (project: string, firewallPolicy: string) =>
20 compute
21 .getRegionNetworkFirewallPolicies({
22 project,
23 region,
24 firewallPolicy,
25 })
26 .pipe(
27 Effect.as("found" as const),
28 Effect.catchTag("NotFound", () => Effect.succeed("gone" as const)),
29 Effect.repeat({
30 schedule: Schedule.spaced("1 second"),
31 until: (status) => status === "gone",
32 times: 10,
33 }),
34 );
35
36const ruleAt = (
37 rules: compute.FirewallPolicyRule[] | undefined,
38 priority: number,
39) => (rules ?? []).find((rule) => rule.priority === priority);
40
41const hasReservedGotoNext = (rules: compute.FirewallPolicyRule[] | undefined) =>
42 (rules ?? []).some(
43 (rule) =>
44 rule.action === "goto_next" &&
45 rule.priority !== undefined &&
46 rule.priority >= 2147483548 &&
47 rule.priority <= 2147483647,
48 );
49
50const nextName = (name: string) => {
51 const flip = name.endsWith("x") ? "z" : "x";
52 return name.length < 63 ? `${name}${flip}` : `${name.slice(0, -1)}${flip}`;
53};
54
55test.provider(
56 "getRegionNetworkFirewallPolicies on a missing policy fails with NotFound",
57 (stack) =>
58 Effect.gen(function* () {
59 yield* stack.destroy();
60 const { project } = yield* GcpEnvironment.current;
61 const error = yield* Effect.flip(
62 compute.getRegionNetworkFirewallPolicies({
63 project,
64 region,
65 firewallPolicy: "alchemy-missing-rnfw",
66 }),
67 );
68 expect(error._tag).toBe("NotFound");
69 yield* stack.destroy();
70 }).pipe(logLevel),
71 { tags: ["provider:gcp", "provider:gcp:compute", "live"], timeout: 90_000 },
72);
73
74test.provider(
75 "create, update, replace, and delete a regional network firewall policy",
76 (stack) =>
77 Effect.gen(function* () {
78 yield* stack.destroy();
79
80 const created = yield* stack.deploy(
81 Effect.gen(function* () {
82 return yield* GCP.Compute.RegionNetworkFirewallPolicy("VpcFw", {
83 region,
84 description: "allow internal http",
85 rules: [
86 {
87 action: "allow",
88 priority: 1000,
89 direction: "INGRESS",
90 match: {
91 srcIpRanges: ["10.0.0.0/8"],
92 layer4Configs: [{ ipProtocol: "tcp", ports: ["80"] }],
93 },
94 },
95 ],
96 });
97 }),
98 );
99
100 expect(created.firewallPolicyName).toEqual(expect.any(String));
101 expect(created.region).toEqual(region);
102 expect(created.policyType).toEqual("VPC_POLICY");
103 expect(created.description).toEqual("allow internal http");
104 expect(ruleAt(created.rules, 1000)?.action).toEqual("allow");
105 expect(hasReservedGotoNext(created.rules)).toEqual(true);
106
107 const fetched = yield* compute.getRegionNetworkFirewallPolicies({
108 project: created.project,
109 region,
110 firewallPolicy: created.firewallPolicyName,
111 });
112 expect(fetched.name).toEqual(created.firewallPolicyName);
113 expect(fetched.description).toContain("[alchemy ");
114 expect(fetched.description).toContain("allow internal http");
115 expect(ruleAt(fetched.rules, 1000)?.match?.srcIpRanges).toEqual(
116 expect.arrayContaining(["10.0.0.0/8"]),
117 );
118
119 const listed = yield* compute.listRegionNetworkFirewallPolicies({
120 project: created.project,
121 region,
122 maxResults: 500,
123 });
124 expect(
125 (listed.items ?? []).some(
126 (policy) => policy.name === created.firewallPolicyName,
127 ),
128 ).toEqual(true);
129
130 const updated = yield* stack.deploy(
131 Effect.gen(function* () {
132 return yield* GCP.Compute.RegionNetworkFirewallPolicy("VpcFw", {
133 firewallPolicyName: created.firewallPolicyName,
134 region,
135 description: "updated vpc fw",
136 rules: [
137 {
138 action: "allow",
139 priority: 1000,
140 direction: "INGRESS",
141 description: "http and https",
142 match: {
143 srcIpRanges: ["10.0.0.0/8", "192.168.0.0/16"],
144 layer4Configs: [{ ipProtocol: "tcp", ports: ["80", "443"] }],
145 },
146 },
147 ],
148 });
149 }),
150 );
151
152 expect(updated.firewallPolicyName).toEqual(created.firewallPolicyName);
153 expect(updated.firewallPolicyId).toEqual(created.firewallPolicyId);
154 expect(updated.description).toEqual("updated vpc fw");
155 expect(
156 ruleAt(updated.rules, 1000)?.match?.srcIpRanges?.slice().sort(),
157 ).toEqual(["10.0.0.0/8", "192.168.0.0/16"]);
158
159 const fetchedUpdate = yield* compute.getRegionNetworkFirewallPolicies({
160 project: updated.project,
161 region,
162 firewallPolicy: updated.firewallPolicyName,
163 });
164 expect(fetchedUpdate.description).toContain("updated vpc fw");
165 expect(ruleAt(fetchedUpdate.rules, 1000)?.description).toEqual(
166 "http and https",
167 );
168
169 const replaced = yield* stack.deploy(
170 Effect.gen(function* () {
171 return yield* GCP.Compute.RegionNetworkFirewallPolicy("VpcFw", {
172 firewallPolicyName: nextName(created.firewallPolicyName),
173 region,
174 description: "replaced vpc fw",
175 rules: [
176 {
177 action: "deny",
178 priority: 1000,
179 direction: "INGRESS",
180 match: {
181 srcIpRanges: ["0.0.0.0/0"],
182 layer4Configs: [{ ipProtocol: "tcp", ports: ["23"] }],
183 },
184 },
185 ],
186 });
187 }),
188 );
189
190 expect(replaced.firewallPolicyName).toEqual(
191 nextName(created.firewallPolicyName),
192 );
193 expect(replaced.firewallPolicyId).not.toEqual(created.firewallPolicyId);
194 expect(replaced.description).toEqual("replaced vpc fw");
195 expect(ruleAt(replaced.rules, 1000)?.action).toEqual("deny");
196
197 yield* stack.destroy();
198
199 const gone = yield* waitUntilGone(
200 replaced.project,
201 replaced.firewallPolicyName,
202 );
203 expect(gone).toEqual("gone");
204 }).pipe(logLevel),
205 { tags: ["provider:gcp", "provider:gcp:compute", "live"], timeout: 180_000 },
206);