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