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// Two pools + volume + replication: 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.getProjectsLocationsVolumesReplications({ 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 "getProjectsLocationsVolumesReplications on a missing replication 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.getProjectsLocationsVolumesReplications({
48 name: `projects/${project}/locations/us-central1/volumes/alchemy-netapp-missing/replications/alchemy-repl-missing`,
49 }),
50 );
51 expect(error._tag).toEqual("NotFound");
52
53 const page = yield* netapp.listProjectsLocationsVolumesReplications({
54 parent: `projects/${project}/locations/us-central1/volumes/-`,
55 pageSize: 10,
56 });
57 expect((page.replications ?? []).map((item) => item.name)).not.toContain(
58 `projects/${project}/locations/us-central1/volumes/alchemy-netapp-missing/replications/alchemy-repl-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 replication",
68 (stack) =>
69 Effect.gen(function* () {
70 yield* stack.destroy();
71
72 const created = yield* stack.deploy(
73 Effect.gen(function* () {
74 const sourcePool = yield* GCP.NetApp.StoragePool("Source", {
75 location: "us-central1",
76 network: "default",
77 serviceLevel: "STANDARD",
78 capacityGib: 2048,
79 labels: { env: "test" },
80 });
81 const destPool = yield* GCP.NetApp.StoragePool("Dest", {
82 location: "us-east4",
83 network: "default",
84 serviceLevel: "STANDARD",
85 capacityGib: 2048,
86 labels: { env: "test" },
87 });
88 const volume = yield* GCP.NetApp.Volume("Share", {
89 location: "us-central1",
90 storagePool: sourcePool.name,
91 protocols: ["NFSV3"],
92 capacityGib: 100,
93 shareName: "alchrepl",
94 labels: { env: "test" },
95 });
96 const replication = yield* GCP.NetApp.VolumesReplication("Dr", {
97 volume: volume.name,
98 replicationSchedule: "HOURLY",
99 destinationVolumeParameters: {
100 storagePool: destPool.name,
101 shareName: "alchrepl",
102 description: "alchemy replica",
103 },
104 description: "alchemy-test-repl",
105 labels: { env: "test" },
106 deleteDestinationVolume: true,
107 });
108 return { sourcePool, destPool, volume, replication };
109 }),
110 );
111
112 expect(created.replication.name).toContain("/replications/");
113 expect(created.replication.replicationId).toEqual(expect.any(String));
114 expect(created.replication.volume).toEqual(created.volume.name);
115 expect(created.replication.description).toEqual("alchemy-test-repl");
116 expect(created.replication.labels).toMatchObject({ env: "test" });
117 expect(created.replication.replicationSchedule).toEqual("HOURLY");
118
119 const fetched = yield* netapp.getProjectsLocationsVolumesReplications({
120 name: created.replication.name,
121 });
122 expect(fetched.name).toEqual(created.replication.name);
123 expect(fetched.description).toEqual("alchemy-test-repl");
124 expect(fetched.labels?.env).toEqual("test");
125 expect(fetched.replicationSchedule).toEqual("HOURLY");
126
127 const updated = yield* stack.deploy(
128 Effect.gen(function* () {
129 const sourcePool = yield* GCP.NetApp.StoragePool("Source", {
130 storagePoolId: created.sourcePool.storagePoolId,
131 location: "us-central1",
132 network: "default",
133 serviceLevel: "STANDARD",
134 capacityGib: 2048,
135 labels: { env: "test" },
136 });
137 const destPool = yield* GCP.NetApp.StoragePool("Dest", {
138 storagePoolId: created.destPool.storagePoolId,
139 location: "us-east4",
140 network: "default",
141 serviceLevel: "STANDARD",
142 capacityGib: 2048,
143 labels: { env: "test" },
144 });
145 const volume = yield* GCP.NetApp.Volume("Share", {
146 volumeId: created.volume.volumeId,
147 location: "us-central1",
148 storagePool: sourcePool.name,
149 protocols: ["NFSV3"],
150 capacityGib: 100,
151 shareName: "alchrepl",
152 labels: { env: "test" },
153 });
154 const replication = yield* GCP.NetApp.VolumesReplication("Dr", {
155 volume: volume.name,
156 replicationId: created.replication.replicationId,
157 replicationSchedule: "DAILY",
158 destinationVolumeParameters: {
159 storagePool: destPool.name,
160 shareName: "alchrepl",
161 },
162 description: "alchemy-prod-repl",
163 labels: { env: "prod", role: "dr" },
164 deleteDestinationVolume: true,
165 });
166 return { sourcePool, destPool, volume, replication };
167 }),
168 );
169
170 expect(updated.replication.name).toEqual(created.replication.name);
171 expect(updated.replication.description).toEqual("alchemy-prod-repl");
172 expect(updated.replication.labels).toMatchObject({
173 env: "prod",
174 role: "dr",
175 });
176 expect(updated.replication.replicationSchedule).toEqual("DAILY");
177
178 const refetched = yield* netapp.getProjectsLocationsVolumesReplications({
179 name: created.replication.name,
180 });
181 expect(refetched.description).toEqual("alchemy-prod-repl");
182 expect(refetched.labels?.env).toEqual("prod");
183 expect(refetched.replicationSchedule).toEqual("DAILY");
184
185 yield* stack.destroy();
186
187 const gone = yield* waitUntilGone(created.replication.name);
188 expect(gone).toEqual("gone");
189 }).pipe(logLevel),
190 { tags: ["provider:gcp", "provider:gcp:netapp", "live"], timeout: 900_000 },
191);