1import { GcpEnvironment } from "@/GCP/Environment";
2import * as GCP from "@/GCP";
3import * as Test from "@/Test/Alchemy";
4import * as file from "@distilled.cloud/gcp/file_v1";
5import { expect } from "alchemy-test";
6import * as Effect from "effect/Effect";
7import { MinimumLogLevel } from "effect/References";
8import * as Schedule from "effect/Schedule";
9import { CAPACITY_ZONE } from "../zones.ts";
10
11const { test } = Test.make({ providers: GCP.providers() });
12
13const logLevel = Effect.provideService(
14 MinimumLogLevel,
15 process.env.DEBUG ? "Debug" : "Info",
16);
17
18// Snapshots need a ZONAL/REGIONAL/ENTERPRISE instance, which draws on the
19// `EnterpriseStorageGibPerRegion` quota. The testing project has none (limit
20// 0), so the lifecycle is entitlement-gated; the probe below pins the typed
21// rejection. Instances also take 5–20 minutes to provision.
22const hasEnterpriseQuota = !!process.env.GCP_TEST_FILESTORE_ENTERPRISE;
23const runLifecycle =
24 hasEnterpriseQuota && !!process.env.GCP_TEST_SLOW && !process.env.FAST;
25
26const waitUntilGone = (name: string) =>
27 file.getProjectsLocationsInstancesSnapshots({ name }).pipe(
28 Effect.as("found" as const),
29 Effect.catchTag("NotFound", () => Effect.succeed("gone" as const)),
30 Effect.repeat({
31 schedule: Schedule.spaced("2 seconds"),
32 until: (status) => status === "gone",
33 times: 10,
34 }),
35 );
36
37test.provider(
38 "getProjectsLocationsInstancesSnapshots on a missing snapshot fails with a typed tag",
39 (stack) =>
40 Effect.gen(function* () {
41 const { project } = yield* GcpEnvironment.current;
42
43 yield* stack.destroy();
44
45 const error = yield* Effect.flip(
46 file.getProjectsLocationsInstancesSnapshots({
47 name: `projects/${project}/locations/us-central1-a/instances/alchemy-filestore-missing/snapshots/alchemy-snap-missing`,
48 }),
49 );
50 expect(error._tag).toEqual("NotFound");
51
52 yield* stack.destroy();
53 }).pipe(logLevel),
54 { tags: ["provider:gcp", "provider:gcp:filestore", "live"], timeout: 90_000 },
55);
56
57test.provider.skipIf(hasEnterpriseQuota || !!process.env.FAST)(
58 "a snapshot-capable instance without enterprise quota fails with StorageQuotaExceeded",
59 (stack) =>
60 Effect.gen(function* () {
61 yield* stack.destroy();
62
63 const error = yield* Effect.flip(
64 stack.deploy(
65 Effect.gen(function* () {
66 return yield* GCP.Filestore.Instance("Nfs", {
67 location: CAPACITY_ZONE,
68 tier: "ZONAL",
69 fileShares: [{ name: "share1", capacityGb: 1024 }],
70 networks: [{ network: "default", modes: ["MODE_IPV4"] }],
71 });
72 }),
73 ),
74 );
75 expect(error._tag).toEqual("StorageQuotaExceeded");
76
77 yield* stack.destroy();
78 }).pipe(logLevel),
79 // If quota is ever granted the deploy creates a real instance; the budget
80 // lets the final destroy remove it instead of abandoning teardown.
81 {
82 tags: ["provider:gcp", "provider:gcp:filestore", "live"],
83 timeout: 2_700_000,
84 retry: 0,
85 },
86);
87
88test.provider.skipIf(!runLifecycle)(
89 "create, update, and delete a filestore snapshot",
90 (stack) =>
91 Effect.gen(function* () {
92 yield* stack.destroy();
93
94 const created = yield* stack.deploy(
95 Effect.gen(function* () {
96 const nfs = yield* GCP.Filestore.Instance("Nfs", {
97 location: CAPACITY_ZONE,
98 tier: "ZONAL",
99 fileShares: [{ name: "share1", capacityGb: 1024 }],
100 networks: [{ network: "default", modes: ["MODE_IPV4"] }],
101 labels: { env: "test" },
102 });
103 const snapshot = yield* GCP.Filestore.InstancesSnapshot("Nightly", {
104 instance: nfs.name,
105 description: "alchemy-test-snap",
106 labels: { env: "test" },
107 });
108 return { nfs, snapshot };
109 }),
110 );
111
112 expect(created.snapshot.name).toContain("/snapshots/");
113 expect(created.snapshot.snapshotId).toEqual(expect.any(String));
114 expect(created.snapshot.instance).toEqual(created.nfs.name);
115 expect(created.snapshot.description).toEqual("alchemy-test-snap");
116 expect(created.snapshot.labels).toMatchObject({ env: "test" });
117 expect(created.snapshot.state).toEqual("READY");
118
119 const fetched = yield* file.getProjectsLocationsInstancesSnapshots({
120 name: created.snapshot.name,
121 });
122 expect(fetched.name).toEqual(created.snapshot.name);
123 expect(fetched.description).toEqual("alchemy-test-snap");
124 expect(fetched.labels?.env).toEqual("test");
125
126 const updated = yield* stack.deploy(
127 Effect.gen(function* () {
128 const nfs = yield* GCP.Filestore.Instance("Nfs", {
129 instanceId: created.nfs.instanceId,
130 location: CAPACITY_ZONE,
131 tier: "ZONAL",
132 fileShares: [{ name: "share1", capacityGb: 1024 }],
133 networks: [{ network: "default", modes: ["MODE_IPV4"] }],
134 labels: { env: "test" },
135 });
136 const snapshot = yield* GCP.Filestore.InstancesSnapshot("Nightly", {
137 instance: nfs.name,
138 snapshotId: created.snapshot.snapshotId,
139 description: "alchemy-prod-snap",
140 labels: { env: "prod", role: "backup" },
141 });
142 return { nfs, snapshot };
143 }),
144 );
145
146 expect(updated.snapshot.name).toEqual(created.snapshot.name);
147 expect(updated.snapshot.description).toEqual("alchemy-prod-snap");
148 expect(updated.snapshot.labels).toMatchObject({
149 env: "prod",
150 role: "backup",
151 });
152
153 const refetched = yield* file.getProjectsLocationsInstancesSnapshots({
154 name: created.snapshot.name,
155 });
156 expect(refetched.description).toEqual("alchemy-prod-snap");
157 expect(refetched.labels?.env).toEqual("prod");
158 expect(refetched.labels?.role).toEqual("backup");
159
160 yield* stack.destroy();
161
162 const gone = yield* waitUntilGone(created.snapshot.name);
163 expect(gone).toEqual("gone");
164 }).pipe(logLevel),
165 {
166 tags: ["provider:gcp", "provider:gcp:filestore", "live"],
167 timeout: 2_700_000,
168 retry: 0,
169 },
170);