1import * as GCP from "@/GCP";
2import { GcpEnvironment } from "@/GCP/Environment";
3import * as Test from "@/Test/Alchemy";
4import * as Core from "@/Test/Core";
5import * as resourcemanager from "@distilled.cloud/gcp/cloudresourcemanager_v3";
6import * as file from "@distilled.cloud/gcp/file_v1";
7import { describe, expect } from "alchemy-test";
8import * as Effect from "effect/Effect";
9import { dockerAvailable, expectProbe } from "../bindingHost.ts";
10import FilestoreBindingsHost, {
11 Nfs,
12 Nightly,
13} from "./fixtures/bindings-host.ts";
14import FilestoreSnapshotBindingsHost, {
15 Snap,
16 ZonalNfs,
17} from "./fixtures/snapshot-host.ts";
18
19const testOptions = { providers: GCP.providers() };
20const { test, beforeAll, afterAll } = Test.make(testOptions);
21const sharedStack = Core.scratchStack(testOptions, "FilestoreBindings");
22const snapshotStack = Core.scratchStack(
23 testOptions,
24 "FilestoreSnapshotBindings",
25);
26
27// Filestore instances take 5–20 minutes to provision.
28const runLifecycle = !!process.env.GCP_TEST_SLOW && !process.env.FAST;
29// Snapshot-capable tiers draw on `EnterpriseStorageGibPerRegion`, which the
30// testing project lacks (limit 0); InstancesSnapshot.test.ts pins the typed
31// StorageQuotaExceeded rejection.
32const runSnapshot = runLifecycle && !!process.env.GCP_TEST_FILESTORE_ENTERPRISE;
33
34let baseUrl: string;
35let hostAccount: string;
36let instanceName: string;
37let backupName: string;
38let snapshotBaseUrl: string;
39let snapshotHostAccount: string;
40let snapshotName: string;
41
42/**
43 * Roles the host's service account holds on the project. Filestore has no
44 * resource-level IAM, so its bindings grant `roles/file.viewer` there.
45 */
46const hostProjectRoles = (account: string) =>
47 Effect.gen(function* () {
48 const { project } = yield* GcpEnvironment.current;
49 const policy = yield* resourcemanager.getIamPolicyProjects({
50 resource: `projects/${project}`,
51 body: { options: { requestedPolicyVersion: 3 } },
52 });
53 return (policy.bindings ?? [])
54 .filter((binding) =>
55 (binding.members ?? []).includes(`serviceAccount:${account}`),
56 )
57 .map((binding) => ({
58 role: binding.role,
59 condition: binding.condition?.expression,
60 }));
61 });
62
63const expectedRoles = [{ role: "roles/file.viewer", condition: undefined }];
64
65describe.skipIf(!dockerAvailable || !runLifecycle)(
66 "Filestore Bindings",
67 {
68 tags: [
69 "provider:gcp",
70 "provider:gcp:filestore",
71 "provider:gcp:run",
72 "live",
73 ],
74 },
75 () => {
76 beforeAll(
77 Effect.gen(function* () {
78 yield* sharedStack.destroy();
79 const out = yield* sharedStack.deploy(
80 Effect.gen(function* () {
81 const host = yield* FilestoreBindingsHost;
82 const instance = yield* Nfs;
83 const backup = yield* Nightly;
84 return {
85 uri: host.uri,
86 serviceAccount: host.serviceAccount,
87 instance: instance.name,
88 backup: backup.name,
89 };
90 }),
91 );
92 baseUrl = out.uri!;
93 hostAccount = out.serviceAccount!;
94 instanceName = out.instance;
95 backupName = out.backup;
96 }),
97 { timeout: 1_800_000 },
98 );
99
100 afterAll(sharedStack.destroy(), { timeout: 1_800_000 });
101
102 describe("GetInstance", () => {
103 test.provider(
104 "reads the instance as the host's service account",
105 (_stack) =>
106 Effect.gen(function* () {
107 const out = yield* expectProbe<file.Instance>(
108 baseUrl,
109 "getInstance",
110 );
111 const direct = yield* file.getProjectsLocationsInstances({
112 name: instanceName,
113 });
114 expect(out.name).toEqual(instanceName);
115 expect(out.tier).toEqual(direct.tier);
116 expect(out.fileShares?.[0]?.name).toEqual("share1");
117 expect(yield* hostProjectRoles(hostAccount)).toEqual(expectedRoles);
118 }),
119 {
120 tags: ["provider:gcp", "provider:gcp:filestore", "live"],
121 timeout: 600_000,
122 },
123 );
124 });
125
126 describe("GetBackup", () => {
127 test.provider(
128 "reads the backup as the host's service account",
129 (_stack) =>
130 Effect.gen(function* () {
131 const out = yield* expectProbe<file.Backup>(baseUrl, "getBackup");
132 const direct = yield* file.getProjectsLocationsBackups({
133 name: backupName,
134 });
135 expect(out.name).toEqual(backupName);
136 expect(out.sourceInstance).toEqual(direct.sourceInstance);
137 expect(out.sourceFileShare).toEqual("share1");
138 expect(yield* hostProjectRoles(hostAccount)).toEqual(expectedRoles);
139 }),
140 {
141 tags: ["provider:gcp", "provider:gcp:filestore", "live"],
142 timeout: 600_000,
143 },
144 );
145 });
146 },
147);
148
149describe.skipIf(!dockerAvailable || !runSnapshot)(
150 "Filestore Bindings (enterprise quota)",
151 {
152 tags: [
153 "provider:gcp",
154 "provider:gcp:filestore",
155 "provider:gcp:run",
156 "live",
157 ],
158 },
159 () => {
160 beforeAll(
161 Effect.gen(function* () {
162 yield* snapshotStack.destroy();
163 const out = yield* snapshotStack.deploy(
164 Effect.gen(function* () {
165 const host = yield* FilestoreSnapshotBindingsHost;
166 yield* ZonalNfs;
167 const snapshot = yield* Snap;
168 return {
169 uri: host.uri,
170 serviceAccount: host.serviceAccount,
171 snapshot: snapshot.name,
172 };
173 }),
174 );
175 snapshotBaseUrl = out.uri!;
176 snapshotHostAccount = out.serviceAccount!;
177 snapshotName = out.snapshot;
178 }),
179 { timeout: 1_800_000 },
180 );
181
182 afterAll(snapshotStack.destroy(), { timeout: 1_800_000 });
183
184 describe("GetInstancesSnapshot", () => {
185 test.provider(
186 "reads the snapshot as the host's service account",
187 (_stack) =>
188 Effect.gen(function* () {
189 const out = yield* expectProbe<file.Snapshot>(
190 snapshotBaseUrl,
191 "getInstancesSnapshot",
192 );
193 const direct = yield* file.getProjectsLocationsInstancesSnapshots({
194 name: snapshotName,
195 });
196 expect(out.name).toEqual(snapshotName);
197 expect(out.createTime).toEqual(direct.createTime);
198 expect(yield* hostProjectRoles(snapshotHostAccount)).toEqual(
199 expectedRoles,
200 );
201 }),
202 {
203 tags: ["provider:gcp", "provider:gcp:filestore", "live"],
204 timeout: 600_000,
205 },
206 );
207 });
208 },
209);