1import * as floatingIps from "@distilled.cloud/hetzner/floating_ips";
2import * as Hetzner from "@/Hetzner";
3import * as Provider from "@/Provider";
4import * as Test from "@/Test/Alchemy";
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 floatingIps.getFloatingIp({ 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 delete unassigned ipv4",
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.FloatingIp("PublicIp", {
39 type: "ipv4",
40 homeLocation: "nbg1",
41 description: "alchemy floating ip create",
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.type).toEqual("ipv4");
50 expect(created.ip).toEqual(expect.any(String));
51 expect(created.homeLocation).toEqual("nbg1");
52 expect(created.homeLocationId).toEqual(expect.any(Number));
53 expect(created.description).toEqual("alchemy floating ip create");
54 expect(created.deleteProtection).toEqual(false);
55 expect(created.serverId).toBeNull();
56 expect(created.labels).toMatchObject({ env: "test" });
57
58 const fetched = yield* floatingIps.getFloatingIp({
59 id: created.id,
60 });
61 expect(fetched.floating_ip.id).toEqual(created.id);
62 expect(fetched.floating_ip.type).toEqual("ipv4");
63 expect(fetched.floating_ip.ip).toEqual(created.ip);
64 expect(fetched.floating_ip.home_location.name).toEqual("nbg1");
65 expect(fetched.floating_ip.description).toEqual(
66 "alchemy floating ip create",
67 );
68 expect(fetched.floating_ip.server).toBeNull();
69 expect(fetched.floating_ip.labels.env).toEqual("test");
70
71 const updated = yield* stack.deploy(
72 Effect.gen(function* () {
73 return yield* Hetzner.FloatingIp("PublicIp", {
74 type: "ipv4",
75 homeLocation: "nbg1",
76 description: "alchemy floating ip update",
77 labels: { env: "prod", role: "edge" },
78 });
79 }),
80 );
81
82 expect(updated.id).toEqual(created.id);
83 expect(updated.ip).toEqual(created.ip);
84 expect(updated.description).toEqual("alchemy floating ip update");
85 expect(updated.labels).toMatchObject({ env: "prod", role: "edge" });
86
87 const refetched = yield* floatingIps.getFloatingIp({
88 id: updated.id,
89 });
90 expect(refetched.floating_ip.description).toEqual(
91 "alchemy floating ip update",
92 );
93 expect(refetched.floating_ip.labels.env).toEqual("prod");
94 expect(refetched.floating_ip.labels.role).toEqual("edge");
95
96 yield* stack.destroy();
97
98 const gone = yield* waitUntilGone(created.id);
99 expect(gone).toEqual("gone");
100 }).pipe(logLevel),
101 {
102 tags: [
103 "provider:hetzner",
104 "provider:hetzner:floatingip",
105 "provider:hetzner:service",
106 "live",
107 ],
108 timeout: 120_000,
109 },
110);
111
112test.provider.skipIf(!hasHetznerCreds)(
113 "replace when type changes",
114 (stack) =>
115 Effect.gen(function* () {
116 yield* stack.destroy();
117
118 const created = yield* stack.deploy(
119 Effect.gen(function* () {
120 return yield* Hetzner.FloatingIp("ReplaceIp", {
121 type: "ipv4",
122 homeLocation: "nbg1",
123 });
124 }),
125 );
126
127 expect(created.type).toEqual("ipv4");
128
129 const replaced = yield* stack.deploy(
130 Effect.gen(function* () {
131 return yield* Hetzner.FloatingIp("ReplaceIp", {
132 type: "ipv6",
133 homeLocation: "nbg1",
134 });
135 }),
136 );
137
138 expect(replaced.id).not.toEqual(created.id);
139 expect(replaced.type).toEqual("ipv6");
140 expect(replaced.ip).not.toEqual(created.ip);
141 expect(replaced.homeLocation).toEqual("nbg1");
142 expect(replaced.serverId).toBeNull();
143
144 const fetched = yield* floatingIps.getFloatingIp({
145 id: replaced.id,
146 });
147 expect(fetched.floating_ip.type).toEqual("ipv6");
148
149 const oldGone = yield* waitUntilGone(created.id);
150 expect(oldGone).toEqual("gone");
151
152 yield* stack.destroy();
153
154 const gone = yield* waitUntilGone(replaced.id);
155 expect(gone).toEqual("gone");
156 }).pipe(logLevel),
157 {
158 tags: [
159 "provider:hetzner",
160 "provider:hetzner:floatingip",
161 "provider:hetzner:service",
162 "live",
163 ],
164 timeout: 120_000,
165 },
166);
167
168test.provider.skipIf(!hasHetznerCreds)(
169 "list enumerates the deployed floating ip",
170 (stack) =>
171 Effect.gen(function* () {
172 yield* stack.destroy();
173
174 const deployed = yield* stack.deploy(
175 Effect.gen(function* () {
176 return yield* Hetzner.FloatingIp("ListIp", {
177 type: "ipv4",
178 homeLocation: "nbg1",
179 });
180 }),
181 );
182
183 const provider = yield* Provider.findProvider(Hetzner.FloatingIp);
184 const all = yield* provider.list();
185 const found = all.find((ip) => ip.id === deployed.id);
186 expect(found).toBeDefined();
187 expect(found?.type).toEqual("ipv4");
188 expect(found?.homeLocation).toEqual("nbg1");
189 expect(found?.ip).toEqual(deployed.ip);
190
191 yield* stack.destroy();
192
193 const gone = yield* waitUntilGone(deployed.id);
194 expect(gone).toEqual("gone");
195 }).pipe(logLevel),
196 {
197 tags: [
198 "provider:hetzner",
199 "provider:hetzner:floatingip",
200 "provider:hetzner:service",
201 "live",
202 ],
203 timeout: 120_000,
204 },
205);