1import * as Hetzner from "@/Hetzner";
2import * as Provider from "@/Provider";
3import * as Test from "@/Test/Alchemy";
4import * as volumes from "@distilled.cloud/hetzner/volumes";
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: Hetzner.providers() });
11
12const logLevel = Effect.provideService(
13 MinimumLogLevel,
14 process.env.DEBUG ? "Debug" : "Info",
15);
16
17const hasHetznerCreds = !!process.env.HCLOUD_TOKEN;
18
19const waitUntilGone = (id: number) =>
20 volumes.getVolume({ id }).pipe(
21 Effect.as("found" as const),
22 Effect.catchTag("NotFound", () => Effect.succeed("gone" as const)),
23 Effect.repeat({
24 schedule: Schedule.spaced("1 second"),
25 until: (status) => status === "gone",
26 times: 10,
27 }),
28 );
29
30test.provider.skipIf(!hasHetznerCreds)(
31 "create, update, and delete an unattached volume",
32 (stack) =>
33 Effect.gen(function* () {
34 yield* stack.destroy();
35
36 const created = yield* stack.deploy(
37 Effect.gen(function* () {
38 return yield* Hetzner.Volume("Data", {
39 size: 10,
40 format: "ext4",
41 location: "nbg1",
42 labels: { env: "test" },
43 });
44 }),
45 );
46
47 expect(created.id).toEqual(expect.any(Number));
48 expect(created.name).toEqual(expect.any(String));
49 expect(created.size).toEqual(10);
50 expect(created.format).toEqual("ext4");
51 expect(created.location).toEqual("nbg1");
52 expect(created.locationId).toEqual(expect.any(Number));
53 expect(created.linuxDevice).toMatch(/^\/dev\//);
54 expect(created.serverId).toBeNull();
55 expect(created.labels).toMatchObject({ env: "test" });
56
57 const fetched = yield* volumes.getVolume({
58 id: created.id,
59 });
60 expect(fetched.volume.id).toEqual(created.id);
61 expect(fetched.volume.size).toEqual(10);
62 expect(fetched.volume.format).toEqual("ext4");
63 expect(fetched.volume.location.name).toEqual("nbg1");
64 expect(fetched.volume.linux_device).toEqual(created.linuxDevice);
65 expect(fetched.volume.server).toBeNull();
66 expect(fetched.volume.labels.env).toEqual("test");
67
68 const updated = yield* stack.deploy(
69 Effect.gen(function* () {
70 return yield* Hetzner.Volume("Data", {
71 size: 20,
72 format: "ext4",
73 location: "nbg1",
74 labels: { env: "prod", role: "data" },
75 });
76 }),
77 );
78
79 expect(updated.id).toEqual(created.id);
80 expect(updated.linuxDevice).toEqual(created.linuxDevice);
81 expect(updated.size).toEqual(20);
82 expect(updated.labels).toMatchObject({ env: "prod", role: "data" });
83
84 const refetched = yield* volumes.getVolume({
85 id: updated.id,
86 });
87 expect(refetched.volume.size).toEqual(20);
88 expect(refetched.volume.linux_device).toEqual(created.linuxDevice);
89 expect(refetched.volume.labels.env).toEqual("prod");
90 expect(refetched.volume.labels.role).toEqual("data");
91
92 yield* stack.destroy();
93
94 const gone = yield* waitUntilGone(created.id);
95 expect(gone).toEqual("gone");
96 }).pipe(logLevel),
97 {
98 tags: [
99 "provider:hetzner",
100 "provider:hetzner:service",
101 "provider:hetzner:volume",
102 "live",
103 ],
104 timeout: 120_000,
105 },
106);
107
108test.provider.skipIf(!hasHetznerCreds)(
109 "replace when format changes",
110 (stack) =>
111 Effect.gen(function* () {
112 yield* stack.destroy();
113
114 const created = yield* stack.deploy(
115 Effect.gen(function* () {
116 return yield* Hetzner.Volume("ReplaceData", {
117 size: 10,
118 format: "ext4",
119 location: "nbg1",
120 });
121 }),
122 );
123
124 expect(created.format).toEqual("ext4");
125
126 const replaced = yield* stack.deploy(
127 Effect.gen(function* () {
128 return yield* Hetzner.Volume("ReplaceData", {
129 size: 10,
130 format: "xfs",
131 location: "nbg1",
132 });
133 }),
134 );
135
136 expect(replaced.id).not.toEqual(created.id);
137 expect(replaced.format).toEqual("xfs");
138 expect(replaced.location).toEqual("nbg1");
139 expect(replaced.serverId).toBeNull();
140
141 const fetched = yield* volumes.getVolume({
142 id: replaced.id,
143 });
144 expect(fetched.volume.format).toEqual("xfs");
145
146 const oldGone = yield* waitUntilGone(created.id);
147 expect(oldGone).toEqual("gone");
148
149 yield* stack.destroy();
150
151 const gone = yield* waitUntilGone(replaced.id);
152 expect(gone).toEqual("gone");
153 }).pipe(logLevel),
154 {
155 tags: [
156 "provider:hetzner",
157 "provider:hetzner:service",
158 "provider:hetzner:volume",
159 "live",
160 ],
161 timeout: 120_000,
162 },
163);
164
165test.provider.skipIf(!hasHetznerCreds)(
166 "list enumerates the deployed volume",
167 (stack) =>
168 Effect.gen(function* () {
169 yield* stack.destroy();
170
171 const deployed = yield* stack.deploy(
172 Effect.gen(function* () {
173 return yield* Hetzner.Volume("ListData", {
174 size: 10,
175 format: "ext4",
176 location: "nbg1",
177 });
178 }),
179 );
180
181 const provider = yield* Provider.findProvider(Hetzner.Volume);
182 const all = yield* provider.list();
183 const found = all.find((volume) => volume.id === deployed.id);
184 expect(found).toBeDefined();
185 expect(found?.size).toEqual(10);
186 expect(found?.format).toEqual("ext4");
187 expect(found?.location).toEqual("nbg1");
188 expect(found?.linuxDevice).toEqual(deployed.linuxDevice);
189
190 yield* stack.destroy();
191
192 const gone = yield* waitUntilGone(deployed.id);
193 expect(gone).toEqual("gone");
194 }).pipe(logLevel),
195 {
196 tags: [
197 "provider:hetzner",
198 "provider:hetzner:service",
199 "provider:hetzner:volume",
200 "live",
201 ],
202 timeout: 120_000,
203 },
204);