1import * as GCP from "@/GCP";
2import * as Test from "@/Test/Alchemy";
3import * as networksecurity from "@distilled.cloud/gcp/networksecurity_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
17// An attachment needs a SAC realm already paired with its SSE partner
18// (Palo Alto Prisma Access / Symantec), which happens outside GCP. Set
19// GCP_TEST_SAC_PARTNER_REALM to the full resource name of such a realm to
20// run the lifecycle. The NCC gateway spoke it needs takes well over 5
21// minutes to provision.
22const partnerRealm = process.env.GCP_TEST_SAC_PARTNER_REALM;
23const runLifecycle =
24 !!partnerRealm && !!process.env.GCP_TEST_SLOW && !process.env.FAST;
25
26const waitUntilGone = (name: string) =>
27 networksecurity.getProjectsLocationsSacAttachments({ name }).pipe(
28 Effect.as("found" as const),
29 Effect.catchTag("NotFound", () => Effect.succeed("gone" as const)),
30 Effect.repeat({
31 schedule: Schedule.spaced("2 seconds"),
32 until: (status) => status === "gone",
33 times: 10,
34 }),
35 );
36
37test.provider(
38 "getProjectsLocationsSacAttachments on a missing attachment fails with a typed tag",
39 (stack) =>
40 Effect.gen(function* () {
41 yield* stack.destroy();
42
43 const { project } = yield* GcpEnvironment.current;
44 const error = yield* Effect.flip(
45 networksecurity.getProjectsLocationsSacAttachments({
46 name: `projects/${project}/locations/us-central1/sacAttachments/alchemy-missing`,
47 }),
48 );
49 expect(error._tag).toEqual("NotFound");
50
51 yield* stack.destroy();
52 }).pipe(logLevel),
53 {
54 tags: ["provider:gcp", "provider:gcp:networksecurity", "live"],
55 timeout: 90_000,
56 },
57);
58
59// GCP validates the gateway before the realm, so proving the typed
60// unpaired-realm rejection needs a real NCC gateway spoke (slow).
61test.provider.skipIf(!process.env.GCP_TEST_SLOW || !!process.env.FAST)(
62 "createProjectsLocationsSacAttachments on an unpaired realm fails with SacRealmNotPaired",
63 (stack) =>
64 Effect.gen(function* () {
65 yield* stack.destroy();
66
67 const { project } = yield* GcpEnvironment.current;
68 const { realm, gateway } = yield* stack.deploy(
69 Effect.gen(function* () {
70 const hub = yield* GCP.NetworkConnectivity.Hub("ProbeMesh", {
71 description: "sac unpaired probe hub",
72 });
73 const gateway = yield* GCP.NetworkConnectivity.Spoke("ProbeGateway", {
74 location: "us-central1",
75 hub: hub.name,
76 gateway: {
77 capacity: "CAPACITY_1_GBPS",
78 ipRangeReservations: [{ ipRange: "10.22.0.0/23" }],
79 },
80 });
81 const realm = yield* GCP.NetworkSecurity.SacRealm("Unpaired", {
82 securityService: "PALO_ALTO_PRISMA_ACCESS",
83 });
84 return { realm, gateway };
85 }),
86 );
87
88 const error = yield* Effect.flip(
89 networksecurity.createProjectsLocationsSacAttachments({
90 parent: `projects/${project}/locations/us-central1`,
91 sacAttachmentId: "alchemy-unpaired-probe",
92 body: { sacRealm: realm.name, nccGateway: gateway.name },
93 }),
94 );
95 expect(error._tag).toEqual("SacRealmNotPaired");
96
97 yield* stack.destroy();
98 }).pipe(logLevel),
99 {
100 tags: ["provider:gcp", "provider:gcp:networksecurity", "live"],
101 timeout: 2_400_000,
102 retry: 0,
103 },
104);
105
106test.provider.skipIf(!runLifecycle)(
107 "create and delete a sac attachment",
108 (stack) =>
109 Effect.gen(function* () {
110 yield* stack.destroy();
111
112 const created = yield* stack.deploy(
113 Effect.gen(function* () {
114 const hub = yield* GCP.NetworkConnectivity.Hub("Mesh", {
115 description: "sac hub",
116 });
117 const gateway = yield* GCP.NetworkConnectivity.Spoke("Gateway", {
118 location: "us-central1",
119 hub: hub.name,
120 gateway: {
121 capacity: "CAPACITY_1_GBPS",
122 ipRangeReservations: [{ ipRange: "10.20.0.0/23" }],
123 },
124 });
125 const attachment = yield* GCP.NetworkSecurity.SacAttachment(
126 "PrismaLink",
127 {
128 location: "us-central1",
129 sacRealm: partnerRealm!,
130 nccGateway: gateway.name,
131 labels: { env: "test" },
132 },
133 );
134 return { hub, gateway, attachment };
135 }),
136 );
137
138 expect(created.attachment.name).toContain("/sacAttachments/");
139 expect(created.attachment.location).toEqual("us-central1");
140 expect(created.attachment.labels).toMatchObject({ env: "test" });
141 expect(created.attachment.sacRealm).toContain(
142 partnerRealm!.split("/").pop()!,
143 );
144
145 const fetched = yield* networksecurity.getProjectsLocationsSacAttachments(
146 {
147 name: created.attachment.name,
148 },
149 );
150 expect(fetched.name).toEqual(created.attachment.name);
151 expect(fetched.labels?.env).toEqual("test");
152 expect(
153 Object.keys(fetched.labels ?? {}).some((key) =>
154 key.startsWith("alchemy-"),
155 ),
156 ).toEqual(true);
157
158 yield* stack.destroy();
159
160 const gone = yield* waitUntilGone(created.attachment.name);
161 expect(gone).toEqual("gone");
162 }).pipe(logLevel),
163 {
164 tags: ["provider:gcp", "provider:gcp:networksecurity", "live"],
165 timeout: 1_200_000,
166 retry: 0,
167 },
168);