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";
8
9const { test } = Test.make({ providers: GCP.providers() });
10
11const logLevel = Effect.provideService(
12 MinimumLogLevel,
13 process.env.DEBUG ? "Debug" : "Info",
14);
15
16const waitUntilGone = (project: string, securityPolicy: string) =>
17 compute.getSecurityPolicies({ project, securityPolicy }).pipe(
18 Effect.as("found" as const),
19 Effect.catchTag("NotFound", () => Effect.succeed("gone" as const)),
20 Effect.repeat({
21 schedule: Schedule.spaced("1 second"),
22 until: (status) => status === "gone",
23 times: 10,
24 }),
25 );
26
27const ruleAt = (
28 rules: compute.SecurityPolicyRule[] | undefined,
29 priority: number,
30) => (rules ?? []).find((rule) => rule.priority === priority);
31
32test.provider(
33 "create, update, replace, and delete a security policy",
34 (stack) =>
35 Effect.gen(function* () {
36 yield* stack.destroy();
37
38 const created = yield* stack.deploy(
39 Effect.gen(function* () {
40 return yield* GCP.Compute.SecurityPolicy("Armor", {
41 description: "deny scanner",
42 labels: { env: "test" },
43 rules: [
44 {
45 action: "deny(403)",
46 priority: 1000,
47 description: "block scanner",
48 match: {
49 versionedExpr: "SRC_IPS_V1",
50 config: { srcIpRanges: ["9.9.9.0/24"] },
51 },
52 },
53 ],
54 });
55 }),
56 );
57
58 expect(created.securityPolicyName).toEqual(expect.any(String));
59 expect(created.type).toEqual("CLOUD_ARMOR");
60 expect(created.description).toEqual("deny scanner");
61 expect(created.labels).toMatchObject({ env: "test" });
62 expect(ruleAt(created.rules, 1000)?.action).toEqual("deny(403)");
63 expect(ruleAt(created.rules, 2147483647)?.action).toEqual("allow");
64
65 const fetched = yield* compute.getSecurityPolicies({
66 project: created.project,
67 securityPolicy: created.securityPolicyName,
68 });
69 expect(fetched.name).toEqual(created.securityPolicyName);
70 expect(fetched.type).toEqual("CLOUD_ARMOR");
71 expect(fetched.description).toEqual("deny scanner");
72 expect(fetched.labels?.env).toEqual("test");
73 expect(fetched.labels?.["alchemy-id"]).toEqual(expect.any(String));
74 expect(ruleAt(fetched.rules, 1000)?.match?.config?.srcIpRanges).toEqual(
75 expect.arrayContaining(["9.9.9.0/24"]),
76 );
77
78 const listed = yield* compute.listSecurityPolicies({
79 project: created.project,
80 filter: "labels.alchemy-id:*",
81 maxResults: 500,
82 });
83 expect(
84 (listed.items ?? []).some(
85 (policy) => policy.name === created.securityPolicyName,
86 ),
87 ).toEqual(true);
88
89 const updated = yield* stack.deploy(
90 Effect.gen(function* () {
91 return yield* GCP.Compute.SecurityPolicy("Armor", {
92 securityPolicyName: created.securityPolicyName,
93 description: "updated armor",
94 labels: { env: "prod", role: "waf" },
95 advancedOptionsConfig: {
96 jsonParsing: "STANDARD",
97 logLevel: "VERBOSE",
98 },
99 rules: [
100 {
101 action: "deny(403)",
102 priority: 1000,
103 description: "block scanners",
104 match: {
105 versionedExpr: "SRC_IPS_V1",
106 config: { srcIpRanges: ["9.9.9.0/24", "8.8.8.8/32"] },
107 },
108 },
109 {
110 action: "allow",
111 priority: 2147483647,
112 match: {
113 versionedExpr: "SRC_IPS_V1",
114 config: { srcIpRanges: ["*"] },
115 },
116 },
117 ],
118 });
119 }),
120 );
121
122 expect(updated.securityPolicyName).toEqual(created.securityPolicyName);
123 expect(updated.securityPolicyId).toEqual(created.securityPolicyId);
124 expect(updated.description).toEqual("updated armor");
125 expect(updated.labels).toMatchObject({ env: "prod", role: "waf" });
126 expect(updated.advancedOptionsConfig?.jsonParsing).toEqual("STANDARD");
127 expect(updated.advancedOptionsConfig?.logLevel).toEqual("VERBOSE");
128 expect(
129 ruleAt(updated.rules, 1000)?.match?.config?.srcIpRanges?.sort(),
130 ).toEqual(["8.8.8.8/32", "9.9.9.0/24"]);
131
132 const fetchedUpdate = yield* compute.getSecurityPolicies({
133 project: updated.project,
134 securityPolicy: updated.securityPolicyName,
135 });
136 expect(fetchedUpdate.description).toEqual("updated armor");
137 expect(fetchedUpdate.labels?.env).toEqual("prod");
138 expect(fetchedUpdate.labels?.role).toEqual("waf");
139 expect(fetchedUpdate.advancedOptionsConfig?.jsonParsing).toEqual(
140 "STANDARD",
141 );
142
143 const replaced = yield* stack.deploy(
144 Effect.gen(function* () {
145 return yield* GCP.Compute.SecurityPolicy("Armor", {
146 securityPolicyName: created.securityPolicyName,
147 type: "CLOUD_ARMOR_EDGE",
148 description: "edge policy",
149 labels: { env: "prod" },
150 rules: [
151 {
152 action: "deny(403)",
153 priority: 1000,
154 match: {
155 versionedExpr: "SRC_IPS_V1",
156 config: { srcIpRanges: ["9.9.9.0/24"] },
157 },
158 },
159 ],
160 });
161 }),
162 );
163
164 expect(replaced.securityPolicyName).toEqual(created.securityPolicyName);
165 expect(replaced.type).toEqual("CLOUD_ARMOR_EDGE");
166 expect(replaced.description).toEqual("edge policy");
167 expect(replaced.securityPolicyId).not.toEqual(created.securityPolicyId);
168
169 const fetchedReplace = yield* compute.getSecurityPolicies({
170 project: replaced.project,
171 securityPolicy: replaced.securityPolicyName,
172 });
173 expect(fetchedReplace.type).toEqual("CLOUD_ARMOR_EDGE");
174 expect(fetchedReplace.description).toEqual("edge policy");
175
176 yield* stack.destroy();
177
178 const gone = yield* waitUntilGone(
179 created.project,
180 created.securityPolicyName,
181 );
182 expect(gone).toEqual("gone");
183 }).pipe(logLevel),
184 { tags: ["provider:gcp", "provider:gcp:compute", "live"], timeout: 90_000 },
185);