1import * as Hetzner from "@/Hetzner";
2import * as Provider from "@/Provider";
3import * as Test from "@/Test/Alchemy";
4import * as images from "@distilled.cloud/hetzner/images";
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 images.getImage({ 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 a snapshot image",
32 (stack) =>
33 Effect.gen(function* () {
34 yield* stack.destroy();
35
36 const created = yield* stack.deploy(
37 Effect.gen(function* () {
38 const server = yield* Hetzner.Server("Web", {
39 serverType: "cpx12",
40 image: "ubuntu-24.04",
41 location: "nbg1",
42 });
43 return yield* Hetzner.Image("Golden", {
44 server,
45 description: "alchemy-image-golden",
46 labels: { env: "test" },
47 });
48 }),
49 );
50
51 expect(created.id).toEqual(expect.any(Number));
52 expect(created.type).toEqual("snapshot");
53 expect(created.status).toEqual("available");
54 expect(created.description).toEqual("alchemy-image-golden");
55 expect(created.createdFromId).toEqual(expect.any(Number));
56 expect(created.diskSize).toEqual(expect.any(Number));
57 expect(created.architecture).toEqual("x86");
58 expect(created.deleteProtection).toEqual(false);
59 expect(created.labels).toMatchObject({ env: "test" });
60
61 const fetched = yield* images.getImage({ id: created.id });
62 expect(fetched.image?.id).toEqual(created.id);
63 expect(fetched.image?.type).toEqual("snapshot");
64 expect(fetched.image?.status).toEqual("available");
65 expect(fetched.image?.description).toEqual("alchemy-image-golden");
66 expect(fetched.image?.created_from?.id).toEqual(created.createdFromId);
67 expect(fetched.image?.labels.env).toEqual("test");
68
69 const updated = yield* stack.deploy(
70 Effect.gen(function* () {
71 const server = yield* Hetzner.Server("Web", {
72 serverType: "cpx12",
73 image: "ubuntu-24.04",
74 location: "nbg1",
75 });
76 return yield* Hetzner.Image("Golden", {
77 server,
78 description: "alchemy-image-golden-v2",
79 labels: { env: "prod", role: "golden" },
80 });
81 }),
82 );
83
84 expect(updated.id).toEqual(created.id);
85 expect(updated.createdFromId).toEqual(created.createdFromId);
86 expect(updated.description).toEqual("alchemy-image-golden-v2");
87 expect(updated.labels).toMatchObject({ env: "prod", role: "golden" });
88
89 const refetched = yield* images.getImage({ id: updated.id });
90 expect(refetched.image?.id).toEqual(created.id);
91 expect(refetched.image?.description).toEqual("alchemy-image-golden-v2");
92 expect(refetched.image?.labels.env).toEqual("prod");
93 expect(refetched.image?.labels.role).toEqual("golden");
94
95 const provider = yield* Provider.findProvider(Hetzner.Image);
96 const all = yield* provider.list();
97 const found = all.find((image) => image.id === created.id);
98 expect(found).toBeDefined();
99 expect(found?.description).toEqual("alchemy-image-golden-v2");
100 expect(found?.type).toEqual("snapshot");
101
102 yield* stack.destroy();
103
104 const gone = yield* waitUntilGone(created.id);
105 expect(gone).toEqual("gone");
106 }).pipe(logLevel),
107 {
108 tags: [
109 "provider:hetzner",
110 "provider:hetzner:image",
111 "provider:hetzner:server",
112 "provider:hetzner:service",
113 "live",
114 ],
115 timeout: 180_000,
116 exclusive: true,
117 },
118);