1import * as Hetzner from "@/Hetzner";
2import * as Test from "@/Test/Alchemy";
3import * as servers from "@distilled.cloud/hetzner/servers";
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";
9import * as HttpClient from "effect/http/HttpClient";
10import Api from "./fixtures/api.ts";
11import { Data, MARKER } from "./fixtures/shared.ts";
12import Worker from "./fixtures/worker.ts";
13
14const { test } = Test.make({ providers: Hetzner.providers() });
15
16const logLevel = Effect.provideService(
17 MinimumLogLevel,
18 process.env.DEBUG ? "Debug" : "Info",
19);
20
21const hasHetznerCreds = !!process.env.HCLOUD_TOKEN;
22
23const waitUntilGone = (id: number) =>
24 servers.getServer({ id }).pipe(
25 Effect.as("found" as const),
26 Effect.catchTag("NotFound", () => Effect.succeed("gone" as const)),
27 Effect.repeat({
28 schedule: Schedule.spaced("1 second"),
29 until: (status) => status === "gone",
30 times: 10,
31 }),
32 );
33
34test.provider.skipIf(!hasHetznerCreds)(
35 "two services on one server share a mounted volume",
36 (stack) =>
37 Effect.gen(function* () {
38 yield* stack.destroy();
39
40 const deployed = yield* stack.deploy(
41 Effect.gen(function* () {
42 const volume = yield* Data;
43 const worker = yield* Worker;
44 const api = yield* Api;
45 return { volume, worker, api };
46 }),
47 );
48
49 expect(deployed.api.serverId).toEqual(deployed.worker.serverId);
50 expect(deployed.api.ipv4).toEqual(expect.any(String));
51 expect(deployed.api.url).toContain(deployed.api.ipv4);
52 expect(deployed.api.port).toEqual(3000);
53 expect(deployed.api.unitName).toEqual(expect.any(String));
54 expect(deployed.api.code.hash).toEqual(expect.any(String));
55 expect(deployed.worker.unitName).not.toEqual(deployed.api.unitName);
56
57 const fetched = yield* servers.getServer({
58 id: deployed.api.serverId,
59 });
60 expect(fetched.server?.id).toEqual(deployed.api.serverId);
61 expect(fetched.server?.public_net.ipv4?.ip).toEqual(deployed.api.ipv4);
62
63 const liveVolume = yield* volumes.getVolume({
64 id: deployed.volume.id,
65 });
66 expect(liveVolume.volume.server).toEqual(deployed.api.serverId);
67 expect(liveVolume.volume.linux_device).toMatch(/^\/dev\//);
68
69 const body = yield* HttpClient.get(deployed.api.url!).pipe(
70 Effect.flatMap((res) =>
71 res.status === 200
72 ? res.json
73 : Effect.fail(new Error(`api returned ${res.status}`)),
74 ),
75 Effect.retry({
76 schedule: Schedule.spaced("2 seconds"),
77 times: 10,
78 }),
79 Effect.map((value) => value as { text: string; path: string }),
80 );
81 expect(body.path).toEqual("/data");
82 expect(body.text).toEqual(MARKER);
83
84 yield* stack.destroy();
85
86 const gone = yield* waitUntilGone(deployed.api.serverId);
87 expect(gone).toEqual("gone");
88 }).pipe(logLevel),
89 {
90 tags: [
91 "provider:hetzner",
92 "provider:hetzner:mountvolume",
93 "provider:hetzner:server",
94 "provider:hetzner:service",
95 "provider:hetzner:volume",
96 "live",
97 ],
98 timeout: 180_000,
99 exclusive: true,
100 },
101);