1import * as GCP from "@/GCP";
2import * as Test from "@/Test/Alchemy";
3import * as acm from "@distilled.cloud/gcp/accesscontextmanager_v1";
4import { expect } from "alchemy-test";
5import * as Effect from "effect/Effect";
6import * as Schedule from "effect/Schedule";
7import { logLevel, projectContext, runLifecycle } from "./common.ts";
8
9const { test } = Test.make({ providers: GCP.providers() });
10
11const waitUntilGone = (name: string) =>
12 acm.getAccessPoliciesServicePerimeters({ name }).pipe(
13 Effect.as("found" as const),
14 Effect.catchTag("NotFound", () => Effect.succeed("gone" as const)),
15 Effect.repeat({
16 schedule: Schedule.spaced("2 seconds"),
17 until: (status) => status === "gone",
18 times: 10,
19 }),
20 );
21
22test.provider(
23 "getAccessPoliciesServicePerimeters on an inaccessible perimeter fails with Forbidden",
24 (stack) =>
25 Effect.gen(function* () {
26 yield* stack.destroy();
27
28 const error = yield* Effect.flip(
29 acm.getAccessPoliciesServicePerimeters({
30 name: "accessPolicies/123456789012/servicePerimeters/alchemy_missing_perimeter",
31 }),
32 );
33 expect(error._tag).toEqual("Forbidden");
34
35 yield* stack.destroy();
36 }).pipe(logLevel),
37 {
38 tags: ["provider:gcp", "provider:gcp:accesscontextmanager", "live"],
39 timeout: 90_000,
40 },
41);
42
43test.provider.skipIf(!runLifecycle)(
44 "create, update, and delete a service perimeter",
45 (stack) =>
46 Effect.gen(function* () {
47 yield* stack.destroy();
48
49 const ctx = yield* projectContext();
50 const scopes =
51 ctx.projectNumber.length > 0
52 ? [`projects/${ctx.projectNumber}`]
53 : undefined;
54
55 const created = yield* stack.deploy(
56 Effect.gen(function* () {
57 const policy = yield* GCP.AccessContextManager.AccessPolicy(
58 "PerimeterPolicy",
59 {
60 title: "perimeter policy",
61 scopes,
62 },
63 );
64 const perimeter =
65 yield* GCP.AccessContextManager.AccessPoliciesServicePerimeter(
66 "Storage",
67 {
68 policy: policy.name,
69 title: "storage perimeter",
70 description: "storage only",
71 status: {
72 restrictedServices: ["storage.googleapis.com"],
73 },
74 },
75 );
76 return { policy, perimeter };
77 }),
78 );
79
80 expect(created.perimeter.name).toContain("/servicePerimeters/");
81 expect(created.perimeter.policy).toEqual(created.policy.name);
82 expect(created.perimeter.title).toEqual("storage perimeter");
83 expect(created.perimeter.description).toEqual("storage only");
84 expect(created.perimeter.status?.restrictedServices).toContain(
85 "storage.googleapis.com",
86 );
87
88 const fetched = yield* acm.getAccessPoliciesServicePerimeters({
89 name: created.perimeter.name,
90 });
91 expect(fetched.name).toEqual(created.perimeter.name);
92 expect(fetched.description).toContain("alchemy-id=");
93 expect(fetched.description).toContain("storage only");
94
95 const updated = yield* stack.deploy(
96 Effect.gen(function* () {
97 const policy = yield* GCP.AccessContextManager.AccessPolicy(
98 "PerimeterPolicy",
99 {
100 title: "perimeter policy",
101 scopes,
102 },
103 );
104 const perimeter =
105 yield* GCP.AccessContextManager.AccessPoliciesServicePerimeter(
106 "Storage",
107 {
108 policy: policy.name,
109 servicePerimeterId: created.perimeter.servicePerimeterId,
110 title: "data perimeter",
111 description: "storage and bigquery",
112 status: {
113 restrictedServices: [
114 "storage.googleapis.com",
115 "bigquery.googleapis.com",
116 ],
117 },
118 },
119 );
120 return { policy, perimeter };
121 }),
122 );
123
124 expect(updated.perimeter.name).toEqual(created.perimeter.name);
125 expect(updated.perimeter.title).toEqual("data perimeter");
126 expect(updated.perimeter.description).toEqual("storage and bigquery");
127 expect(updated.perimeter.status?.restrictedServices).toEqual(
128 expect.arrayContaining([
129 "storage.googleapis.com",
130 "bigquery.googleapis.com",
131 ]),
132 );
133
134 yield* stack.destroy();
135
136 const gone = yield* waitUntilGone(created.perimeter.name);
137 expect(gone).toEqual("gone");
138 }).pipe(logLevel),
139 {
140 tags: ["provider:gcp", "provider:gcp:accesscontextmanager", "live"],
141 timeout: 120_000,
142 },
143);