1import * as primaryIps from "@distilled.cloud/hetzner/primary_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 primaryIps.getPrimaryIp({ 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 unassigned ipv6 primary ip",
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.PrimaryIp("WebIp", {
39 type: "ipv6",
40 location: "nbg1",
41 labels: { env: "test" },
42 });
43 }),
44 );
45
46 expect(created.id).toEqual(expect.any(Number));
47 expect(created.name).toEqual(expect.any(String));
48 expect(created.type).toEqual("ipv6");
49 expect(created.ip).toEqual(expect.any(String));
50 expect(created.location).toEqual("nbg1");
51 expect(created.locationId).toEqual(expect.any(Number));
52 expect(created.autoDelete).toEqual(false);
53 expect(created.deleteProtection).toEqual(false);
54 expect(created.assigneeId).toBeNull();
55 expect(created.labels).toMatchObject({ env: "test" });
56
57 const fetched = yield* primaryIps.getPrimaryIp({
58 id: created.id,
59 });
60 expect(fetched.primary_ip.id).toEqual(created.id);
61 expect(fetched.primary_ip.type).toEqual("ipv6");
62 expect(fetched.primary_ip.ip).toEqual(created.ip);
63 expect(fetched.primary_ip.location.name).toEqual("nbg1");
64 expect(fetched.primary_ip.auto_delete).toEqual(false);
65 expect(fetched.primary_ip.assignee_id).toBeNull();
66 expect(fetched.primary_ip.labels.env).toEqual("test");
67
68 const updated = yield* stack.deploy(
69 Effect.gen(function* () {
70 return yield* Hetzner.PrimaryIp("WebIp", {
71 type: "ipv6",
72 location: "nbg1",
73 autoDelete: true,
74 labels: { env: "prod", role: "web" },
75 });
76 }),
77 );
78
79 expect(updated.id).toEqual(created.id);
80 expect(updated.ip).toEqual(created.ip);
81 expect(updated.autoDelete).toEqual(true);
82 expect(updated.labels).toMatchObject({ env: "prod", role: "web" });
83
84 const refetched = yield* primaryIps.getPrimaryIp({
85 id: updated.id,
86 });
87 expect(refetched.primary_ip.auto_delete).toEqual(true);
88 expect(refetched.primary_ip.labels.env).toEqual("prod");
89 expect(refetched.primary_ip.labels.role).toEqual("web");
90
91 yield* stack.destroy();
92
93 const gone = yield* waitUntilGone(created.id);
94 expect(gone).toEqual("gone");
95 }).pipe(logLevel),
96 {
97 tags: [
98 "provider:hetzner",
99 "provider:hetzner:primaryip",
100 "provider:hetzner:service",
101 "live",
102 ],
103 timeout: 180_000,
104 },
105);
106
107test.provider.skipIf(!hasHetznerCreds)(
108 "replace when location changes",
109 (stack) =>
110 Effect.gen(function* () {
111 yield* stack.destroy();
112
113 const created = yield* stack.deploy(
114 Effect.gen(function* () {
115 return yield* Hetzner.PrimaryIp("ReplaceIp", {
116 type: "ipv6",
117 location: "nbg1",
118 });
119 }),
120 );
121
122 expect(created.location).toEqual("nbg1");
123 const previousId = Number(created.id);
124 const previousIp = String(created.ip);
125
126 const replaced = yield* stack.deploy(
127 Effect.gen(function* () {
128 return yield* Hetzner.PrimaryIp("ReplaceIp", {
129 type: "ipv6",
130 location: "fsn1",
131 });
132 }),
133 );
134
135 expect(replaced.id).not.toEqual(previousId);
136 expect(replaced.type).toEqual("ipv6");
137 expect(replaced.ip).not.toEqual(previousIp);
138 expect(replaced.location).toEqual("fsn1");
139 expect(replaced.assigneeId).toBeNull();
140
141 const fetched = yield* primaryIps.getPrimaryIp({
142 id: replaced.id,
143 });
144 expect(fetched.primary_ip.location.name).toEqual("fsn1");
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:primaryip",
158 "provider:hetzner:service",
159 "live",
160 ],
161 timeout: 180_000,
162 },
163);
164
165test.provider.skipIf(!hasHetznerCreds)(
166 "create from datacenter name",
167 (stack) =>
168 Effect.gen(function* () {
169 yield* stack.destroy();
170
171 const created = yield* stack.deploy(
172 Effect.gen(function* () {
173 return yield* Hetzner.PrimaryIp("DatacenterIp", {
174 type: "ipv6",
175 datacenter: "nbg1-dc3",
176 });
177 }),
178 );
179
180 expect(created.location).toEqual("nbg1");
181 expect(created.datacenter).toEqual("nbg1-dc3");
182 expect(created.type).toEqual("ipv6");
183 expect(created.assigneeId).toBeNull();
184
185 const fetched = yield* primaryIps.getPrimaryIp({
186 id: created.id,
187 });
188 expect(fetched.primary_ip.location.name).toEqual("nbg1");
189
190 yield* stack.destroy();
191
192 const gone = yield* waitUntilGone(created.id);
193 expect(gone).toEqual("gone");
194 }).pipe(logLevel),
195 {
196 tags: [
197 "provider:hetzner",
198 "provider:hetzner:primaryip",
199 "provider:hetzner:service",
200 "live",
201 ],
202 timeout: 180_000,
203 },
204);
205
206test.provider.skipIf(!hasHetznerCreds)(
207 "list enumerates the deployed primary ip",
208 (stack) =>
209 Effect.gen(function* () {
210 yield* stack.destroy();
211
212 const deployed = yield* stack.deploy(
213 Effect.gen(function* () {
214 return yield* Hetzner.PrimaryIp("ListIp", {
215 type: "ipv6",
216 location: "nbg1",
217 });
218 }),
219 );
220
221 const provider = yield* Provider.findProvider(Hetzner.PrimaryIp);
222 const all = yield* provider.list();
223 const found = all.find((ip) => ip.id === deployed.id);
224 expect(found).toBeDefined();
225 expect(found?.type).toEqual("ipv6");
226 expect(found?.location).toEqual("nbg1");
227 expect(found?.ip).toEqual(deployed.ip);
228
229 yield* stack.destroy();
230
231 const gone = yield* waitUntilGone(deployed.id);
232 expect(gone).toEqual("gone");
233 }).pipe(logLevel),
234 {
235 tags: [
236 "provider:hetzner",
237 "provider:hetzner:primaryip",
238 "provider:hetzner:service",
239 "live",
240 ],
241 timeout: 180_000,
242 },
243);