1import * as GCP from "@/GCP";
2import * as Test from "@/Test/Alchemy";
3import * as orgpolicy from "@distilled.cloud/gcp/orgpolicy_v2";
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// Project org policies need orgpolicy.policies.create, which only an org-level
18// Organization Policy Administrator grant provides; the testing credentials get
19// Forbidden "Permission 'orgpolicy.policies.create' denied on resource ...".
20// Set GOOGLE_ORGANIZATION_ID when the credentials administer the org.
21const runLifecycle = !!process.env.GOOGLE_ORGANIZATION_ID;
22
23const CONSTRAINT_A = "compute.disableSerialPortAccess";
24const CONSTRAINT_B = "compute.disableSerialPortLogging";
25
26const waitUntilGone = (name: string) =>
27 orgpolicy.getProjectsPolicies({ name }).pipe(
28 Effect.as("found" as const),
29 Effect.catchTag("NotFound", () => Effect.succeed("gone" as const)),
30 Effect.repeat({
31 schedule: Schedule.spaced("1 second"),
32 until: (status) => status === "gone",
33 times: 10,
34 }),
35 );
36
37test.provider(
38 "getProjectsPolicies on a missing policy fails with NotFound",
39 (stack) =>
40 Effect.gen(function* () {
41 const { project } = yield* GcpEnvironment.current;
42 yield* stack.destroy();
43
44 const error = yield* Effect.flip(
45 orgpolicy.getProjectsPolicies({
46 name: `projects/${project}/policies/compute.alchemyTestDoesNotExist`,
47 }),
48 );
49 expect(error._tag).toBe("NotFound");
50
51 const page = yield* orgpolicy.listProjectsPolicies({
52 parent: `projects/${project}`,
53 pageSize: 10,
54 });
55 expect((page.policies ?? []).map((policy) => policy.name)).not.toContain(
56 `projects/${project}/policies/compute.alchemyTestDoesNotExist`,
57 );
58
59 yield* stack.destroy();
60 }).pipe(logLevel),
61 { tags: ["provider:gcp", "provider:gcp:orgpolicy", "live"], timeout: 90_000 },
62);
63
64test.provider.skipIf(runLifecycle)(
65 "createProjectsPolicies without orgpolicy.policies.create fails with Forbidden",
66 () =>
67 Effect.gen(function* () {
68 const { project } = yield* GcpEnvironment.current;
69 const error = yield* Effect.flip(
70 orgpolicy.createProjectsPolicies({
71 parent: `projects/${project}`,
72 body: {
73 name: `projects/${project}/policies/${CONSTRAINT_A}`,
74 spec: { rules: [{ enforce: false }] },
75 },
76 }),
77 );
78 expect(error._tag).toBe("Forbidden");
79 }).pipe(logLevel),
80 { tags: ["provider:gcp", "provider:gcp:orgpolicy", "live"] },
81);
82
83test.provider.skipIf(!runLifecycle)(
84 "create, update, replace, and delete a project org policy",
85 (stack) =>
86 Effect.gen(function* () {
87 const { project } = yield* GcpEnvironment.current;
88 yield* stack.destroy();
89
90 const created = yield* stack.deploy(
91 Effect.gen(function* () {
92 return yield* GCP.OrgPolicy.Policy("SerialPort", {
93 constraint: CONSTRAINT_A,
94 spec: {
95 rules: [{ enforce: false }],
96 },
97 });
98 }),
99 );
100
101 expect(created.constraint).toEqual(CONSTRAINT_A);
102 expect(created.parent).toMatch(/^projects\//);
103 expect(created.name).toContain(`/policies/${CONSTRAINT_A}`);
104 expect(created.project).toEqual(project);
105 expect(created.spec?.rules?.[0]?.enforce).toEqual(false);
106
107 const fetched = yield* orgpolicy.getProjectsPolicies({
108 name: created.name,
109 });
110 expect(fetched.name).toEqual(created.name);
111 expect(fetched.spec?.rules?.[0]?.enforce).toEqual(false);
112
113 const updated = yield* stack.deploy(
114 Effect.gen(function* () {
115 return yield* GCP.OrgPolicy.Policy("SerialPort", {
116 constraint: CONSTRAINT_A,
117 spec: {
118 rules: [{ enforce: true }],
119 },
120 });
121 }),
122 );
123
124 expect(updated.name).toEqual(created.name);
125 expect(updated.constraint).toEqual(CONSTRAINT_A);
126 expect(updated.spec?.rules?.[0]?.enforce).toEqual(true);
127
128 const fetchedUpdate = yield* orgpolicy.getProjectsPolicies({
129 name: updated.name,
130 });
131 expect(fetchedUpdate.spec?.rules?.[0]?.enforce).toEqual(true);
132
133 const replaced = yield* stack.deploy(
134 Effect.gen(function* () {
135 return yield* GCP.OrgPolicy.Policy("SerialPort", {
136 constraint: CONSTRAINT_B,
137 spec: {
138 rules: [{ enforce: true }],
139 },
140 });
141 }),
142 );
143
144 expect(replaced.constraint).toEqual(CONSTRAINT_B);
145 expect(replaced.name).toContain(`/policies/${CONSTRAINT_B}`);
146 expect(replaced.name).not.toEqual(created.name);
147 expect(replaced.spec?.rules?.[0]?.enforce).toEqual(true);
148
149 const fetchedReplace = yield* orgpolicy.getProjectsPolicies({
150 name: replaced.name,
151 });
152 expect(fetchedReplace.name).toEqual(replaced.name);
153 expect(fetchedReplace.spec?.rules?.[0]?.enforce).toEqual(true);
154
155 const oldGone = yield* waitUntilGone(created.name);
156 expect(oldGone).toEqual("gone");
157
158 yield* stack.destroy();
159
160 const gone = yield* waitUntilGone(replaced.name);
161 expect(gone).toEqual("gone");
162 }).pipe(logLevel),
163 { tags: ["provider:gcp", "provider:gcp:orgpolicy", "live"], timeout: 90_000 },
164);