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