1import { GcpEnvironment } from "@/GCP/Environment";
2import * as GCP from "@/GCP";
3import * as Test from "@/Test/Alchemy";
4import * as netapp from "@distilled.cloud/gcp/netapp_v1";
5import { expect } from "alchemy-test";
6import * as Effect from "effect/Effect";
7import { MinimumLogLevel } from "effect/References";
8import * as Schedule from "effect/Schedule";
9
10const { test } = Test.make({ providers: GCP.providers() });
11
12const logLevel = Effect.provideService(
13 MinimumLogLevel,
14 process.env.DEBUG ? "Debug" : "Info",
15);
16
17// Pool + volume provisioning is multi-minute; skip unless explicitly enabled.
18// Volumes need a Private Service Access connection to
19// `netapp.servicenetworking.goog` on the `default` network (otherwise volume
20// create fails with `Please create Service Networking connection with
21// service 'netapp.servicenetworking.goog' ...`), and each file provisions its
22// own 2 TiB STANDARD pool while the default regional quota
23// (StandardStoragePoolCapacityGiBPerRegion = 2048) fits one. Set
24// GCP_TEST_NETAPP_VOLUMES=1 on a project with both.
25const runLifecycle = !!process.env.GCP_TEST_NETAPP_VOLUMES && !process.env.FAST;
26
27const waitUntilGone = (name: string) =>
28 netapp.getProjectsLocationsVolumesSnapshots({ name }).pipe(
29 Effect.as("found" as const),
30 Effect.catchTag("NotFound", () => Effect.succeed("gone" as const)),
31 Effect.repeat({
32 schedule: Schedule.spaced("2 seconds"),
33 until: (status) => status === "gone",
34 times: 10,
35 }),
36 );
37
38test.provider(
39 "getProjectsLocationsVolumesSnapshots on a missing snapshot fails with a typed tag",
40 (stack) =>
41 Effect.gen(function* () {
42 const { project } = yield* GcpEnvironment.current;
43
44 yield* stack.destroy();
45
46 const error = yield* Effect.flip(
47 netapp.getProjectsLocationsVolumesSnapshots({
48 name: `projects/${project}/locations/us-central1/volumes/alchemy-netapp-missing/snapshots/alchemy-snap-missing`,
49 }),
50 );
51 expect(error._tag).toEqual("NotFound");
52
53 const page = yield* netapp.listProjectsLocationsVolumesSnapshots({
54 parent: `projects/${project}/locations/us-central1/volumes/-`,
55 pageSize: 10,
56 });
57 expect((page.snapshots ?? []).map((item) => item.name)).not.toContain(
58 `projects/${project}/locations/us-central1/volumes/alchemy-netapp-missing/snapshots/alchemy-snap-missing`,
59 );
60
61 yield* stack.destroy();
62 }).pipe(logLevel),
63 { tags: ["provider:gcp", "provider:gcp:netapp", "live"], timeout: 90_000 },
64);
65
66test.provider.skipIf(!runLifecycle)(
67 "create, update, and delete a volume snapshot",
68 (stack) =>
69 Effect.gen(function* () {
70 yield* stack.destroy();
71
72 const created = yield* stack.deploy(
73 Effect.gen(function* () {
74 const pool = yield* GCP.NetApp.StoragePool("Pool", {
75 location: "us-central1",
76 network: "default",
77 serviceLevel: "STANDARD",
78 capacityGib: 2048,
79 labels: { env: "test" },
80 });
81 const volume = yield* GCP.NetApp.Volume("Share", {
82 location: "us-central1",
83 storagePool: pool.name,
84 protocols: ["NFSV3"],
85 capacityGib: 100,
86 shareName: "alchsnap",
87 labels: { env: "test" },
88 });
89 const snapshot = yield* GCP.NetApp.VolumesSnapshot("Nightly", {
90 volume: volume.name,
91 description: "alchemy-test-snap",
92 labels: { env: "test" },
93 });
94 return { pool, volume, snapshot };
95 }),
96 );
97
98 expect(created.snapshot.name).toContain("/snapshots/");
99 expect(created.snapshot.snapshotId).toEqual(expect.any(String));
100 expect(created.snapshot.volume).toEqual(created.volume.name);
101 expect(created.snapshot.description).toEqual("alchemy-test-snap");
102 expect(created.snapshot.labels).toMatchObject({ env: "test" });
103
104 const fetched = yield* netapp.getProjectsLocationsVolumesSnapshots({
105 name: created.snapshot.name,
106 });
107 expect(fetched.name).toEqual(created.snapshot.name);
108 expect(fetched.description).toEqual("alchemy-test-snap");
109 expect(fetched.labels?.env).toEqual("test");
110
111 const updated = yield* stack.deploy(
112 Effect.gen(function* () {
113 const pool = yield* GCP.NetApp.StoragePool("Pool", {
114 storagePoolId: created.pool.storagePoolId,
115 location: "us-central1",
116 network: "default",
117 serviceLevel: "STANDARD",
118 capacityGib: 2048,
119 labels: { env: "test" },
120 });
121 const volume = yield* GCP.NetApp.Volume("Share", {
122 volumeId: created.volume.volumeId,
123 location: "us-central1",
124 storagePool: pool.name,
125 protocols: ["NFSV3"],
126 capacityGib: 100,
127 shareName: "alchsnap",
128 labels: { env: "test" },
129 });
130 const snapshot = yield* GCP.NetApp.VolumesSnapshot("Nightly", {
131 volume: volume.name,
132 snapshotId: created.snapshot.snapshotId,
133 description: "alchemy-prod-snap",
134 labels: { env: "prod", role: "backup" },
135 });
136 return { pool, volume, snapshot };
137 }),
138 );
139
140 expect(updated.snapshot.name).toEqual(created.snapshot.name);
141 expect(updated.snapshot.description).toEqual("alchemy-prod-snap");
142 expect(updated.snapshot.labels).toMatchObject({
143 env: "prod",
144 role: "backup",
145 });
146
147 const refetched = yield* netapp.getProjectsLocationsVolumesSnapshots({
148 name: created.snapshot.name,
149 });
150 expect(refetched.description).toEqual("alchemy-prod-snap");
151 expect(refetched.labels?.env).toEqual("prod");
152 expect(refetched.labels?.role).toEqual("backup");
153
154 yield* stack.destroy();
155
156 const gone = yield* waitUntilGone(created.snapshot.name);
157 expect(gone).toEqual("gone");
158 }).pipe(logLevel),
159 { tags: ["provider:gcp", "provider:gcp:netapp", "live"], timeout: 900_000 },
160);