1import * as GCP from "@/GCP";
2import * as Test from "@/Test/Alchemy";
3import * as osconfig from "@distilled.cloud/gcp/osconfig_v2";
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 = (name: string) =>
17 osconfig.getOrganizationsLocationsGlobalPolicyOrchestrators({ name }).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
27// The OS Config API is disabled on the testing project (every call fails
28// with ServiceDisabled). Set GCP_TEST_OSCONFIG=1 and GOOGLE_ORGANIZATION_ID
29// on a project with OS Config enabled and org-level OS Config roles.
30const organizationId = process.env.GOOGLE_ORGANIZATION_ID;
31const runLifecycle = !!process.env.GCP_TEST_OSCONFIG && !!organizationId;
32
33const organizationOf = () =>
34 Effect.succeed(
35 organizationId === undefined
36 ? ""
37 : organizationId.startsWith("organizations/")
38 ? organizationId
39 : `organizations/${organizationId}`,
40 );
41
42test.provider(
43 "getOrganizationsLocationsGlobalPolicyOrchestrators on a missing orchestrator fails with a typed tag",
44 (stack) =>
45 Effect.gen(function* () {
46 yield* stack.destroy();
47
48 const organization = (yield* organizationOf()) || "organizations/0";
49 const error = yield* Effect.flip(
50 osconfig.getOrganizationsLocationsGlobalPolicyOrchestrators({
51 name: `${organization}/locations/global/policyOrchestrators/alchemy-missing-orch`,
52 }),
53 );
54 expect(error._tag).toEqual("ServiceDisabled");
55
56 yield* stack.destroy();
57 }).pipe(logLevel),
58 { tags: ["provider:gcp", "provider:gcp:osconfig", "live"], timeout: 90_000 },
59);
60
61test.provider.skipIf(!runLifecycle)(
62 "create, update, and delete an organization policy orchestrator",
63 (stack) =>
64 Effect.gen(function* () {
65 yield* stack.destroy();
66
67 const organization = yield* organizationOf();
68
69 const created = yield* stack.deploy(
70 Effect.gen(function* () {
71 return yield* GCP.OSConfig.OrganizationsLocationsGlobalPolicyOrchestrator(
72 "Debian",
73 {
74 organizationId: organization,
75 description: "org validation",
76 labels: { env: "test" },
77 state: "STOPPED",
78 },
79 );
80 }),
81 );
82
83 expect(created.policyOrchestratorId).toEqual(expect.any(String));
84 expect(created.organizationId).toEqual(
85 organization.replace("organizations/", ""),
86 );
87 expect(created.parent).toEqual(`${organization}/locations/global`);
88 expect(created.name).toEqual(
89 `${organization}/locations/global/policyOrchestrators/${created.policyOrchestratorId}`,
90 );
91 expect(created.state).toEqual("STOPPED");
92 expect(created.labels).toMatchObject({ env: "test" });
93
94 const fetched =
95 yield* osconfig.getOrganizationsLocationsGlobalPolicyOrchestrators({
96 name: created.name,
97 });
98 expect(fetched.name).toEqual(created.name);
99 expect(fetched.labels?.env).toEqual("test");
100
101 const updated = yield* stack.deploy(
102 Effect.gen(function* () {
103 return yield* GCP.OSConfig.OrganizationsLocationsGlobalPolicyOrchestrator(
104 "Debian",
105 {
106 organizationId: organization,
107 policyOrchestratorId: created.policyOrchestratorId,
108 description: "updated org",
109 labels: { env: "prod" },
110 state: "STOPPED",
111 },
112 );
113 }),
114 );
115
116 expect(updated.name).toEqual(created.name);
117 expect(updated.description).toEqual("updated org");
118 expect(updated.labels).toMatchObject({ env: "prod" });
119
120 yield* stack.destroy();
121 const gone = yield* waitUntilGone(created.name);
122 expect(gone).toEqual("gone");
123 }).pipe(logLevel),
124 { tags: ["provider:gcp", "provider:gcp:osconfig", "live"], timeout: 90_000 },
125);