Open History to see the last commit
1import * as Hetzner from "@alchemy.run/hetzner";
2import { expect } from "@alchemy.run/test";
3import * as Test from "@alchemy.run/test/Alchemy";
4import * as networks from "@distilled.cloud/hetzner/networks";
5import * as Provider from "alchemy/Provider";
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(MinimumLogLevel, process.env.DEBUG ? "Debug" : "Info");
13
14const hasHetznerCreds = !!process.env.HCLOUD_TOKEN;
15
16const waitUntilGone = (id: number) =>
17 networks.getNetwork({ id }).pipe(
18 Effect.as("found" as const),
19 Effect.catchTag("NotFound", () => Effect.succeed("gone" as const)),
20 Effect.repeat({
21 schedule: Schedule.spaced("1 second"),
22 until: (status) => status === "gone",
23 times: 10,
24 }),
25 );
26
27test.provider.skipIf(!hasHetznerCreds)(
28 "create, update, and delete a network",
29 (stack) =>
30 Effect.gen(function* () {
31 yield* stack.destroy();
32
33 const created = yield* stack.deploy(
34 Effect.gen(function* () {
35 return yield* Hetzner.Network("Vpc", {
36 ipRange: "10.0.0.0/16",
37 subnets: [
38 {
39 type: "cloud",
40 ipRange: "10.0.1.0/24",
41 networkZone: "eu-central",
42 },
43 ],
44 labels: { env: "test" },
45 });
46 }),
47 );
48
49 expect(created.networkId).toEqual(expect.any(Number));
50 expect(created.name).toEqual(expect.any(String));
51 expect(created.ipRange).toEqual("10.0.0.0/16");
52 expect(created.subnets).toEqual(
53 expect.arrayContaining([
54 expect.objectContaining({
55 type: "cloud",
56 ipRange: "10.0.1.0/24",
57 networkZone: "eu-central",
58 }),
59 ]),
60 );
61 expect(created.routes).toEqual([]);
62 expect(created.deleteProtection).toEqual(false);
63 expect(created.exposeRoutesToVswitch).toEqual(false);
64 expect(created.labels).toMatchObject({ env: "test" });
65
66 const fetched = yield* networks.getNetwork({
67 id: created.networkId,
68 });
69 expect(fetched.network?.id).toEqual(created.networkId);
70 expect(fetched.network?.ip_range).toEqual("10.0.0.0/16");
71 expect(fetched.network?.labels.env).toEqual("test");
72 expect(fetched.network?.subnets).toEqual(
73 expect.arrayContaining([
74 expect.objectContaining({
75 type: "cloud",
76 ip_range: "10.0.1.0/24",
77 network_zone: "eu-central",
78 }),
79 ]),
80 );
81
82 const updated = yield* stack.deploy(
83 Effect.gen(function* () {
84 return yield* Hetzner.Network("Vpc", {
85 ipRange: "10.0.0.0/16",
86 subnets: [
87 {
88 type: "cloud",
89 ipRange: "10.0.1.0/24",
90 networkZone: "eu-central",
91 },
92 {
93 type: "cloud",
94 ipRange: "10.0.2.0/24",
95 networkZone: "eu-central",
96 },
97 ],
98 routes: [{ destination: "10.10.0.0/24", gateway: "10.0.1.2" }],
99 exposeRoutesToVswitch: true,
100 deleteProtection: true,
101 labels: { env: "prod", role: "vpc" },
102 });
103 }),
104 );
105
106 expect(updated.networkId).toEqual(created.networkId);
107 expect(updated.ipRange).toEqual("10.0.0.0/16");
108 expect(updated.subnets).toHaveLength(2);
109 expect(updated.routes).toEqual([{ destination: "10.10.0.0/24", gateway: "10.0.1.2" }]);
110 expect(updated.deleteProtection).toEqual(true);
111 expect(updated.exposeRoutesToVswitch).toEqual(true);
112 expect(updated.labels).toMatchObject({ env: "prod", role: "vpc" });
113
114 const refetched = yield* networks.getNetwork({
115 id: updated.networkId,
116 });
117 expect(refetched.network?.protection.delete).toEqual(true);
118 expect(refetched.network?.expose_routes_to_vswitch).toEqual(true);
119 expect(refetched.network?.labels.env).toEqual("prod");
120 expect(refetched.network?.labels.role).toEqual("vpc");
121 expect(refetched.network?.subnets).toHaveLength(2);
122 expect(refetched.network?.routes).toEqual([
123 { destination: "10.10.0.0/24", gateway: "10.0.1.2" },
124 ]);
125
126 yield* stack.destroy();
127
128 const gone = yield* waitUntilGone(created.networkId);
129 expect(gone).toEqual("gone");
130 }).pipe(logLevel),
131 {
132 tags: ["provider:hetzner", "provider:hetzner:network", "provider:hetzner:service", "live"],
133 timeout: 120_000,
134 },
135);
136
137test.provider.skipIf(!hasHetznerCreds)(
138 "replace when ip range is not a supernet",
139 (stack) =>
140 Effect.gen(function* () {
141 yield* stack.destroy();
142
143 const created = yield* stack.deploy(
144 Effect.gen(function* () {
145 return yield* Hetzner.Network("ReplaceVpc", {
146 ipRange: "10.0.0.0/16",
147 subnets: [
148 {
149 type: "cloud",
150 ipRange: "10.0.1.0/24",
151 networkZone: "eu-central",
152 },
153 ],
154 });
155 }),
156 );
157
158 expect(created.ipRange).toEqual("10.0.0.0/16");
159
160 const replaced = yield* stack.deploy(
161 Effect.gen(function* () {
162 return yield* Hetzner.Network("ReplaceVpc", {
163 ipRange: "172.16.0.0/16",
164 subnets: [
165 {
166 type: "cloud",
167 ipRange: "172.16.1.0/24",
168 networkZone: "eu-central",
169 },
170 ],
171 });
172 }),
173 );
174
175 expect(replaced.networkId).not.toEqual(created.networkId);
176 expect(replaced.ipRange).toEqual("172.16.0.0/16");
177
178 const fetched = yield* networks.getNetwork({
179 id: replaced.networkId,
180 });
181 expect(fetched.network?.ip_range).toEqual("172.16.0.0/16");
182
183 const oldGone = yield* waitUntilGone(created.networkId);
184 expect(oldGone).toEqual("gone");
185
186 yield* stack.destroy();
187
188 const gone = yield* waitUntilGone(replaced.networkId);
189 expect(gone).toEqual("gone");
190 }).pipe(logLevel),
191 {
192 tags: ["provider:hetzner", "provider:hetzner:network", "provider:hetzner:service", "live"],
193 timeout: 120_000,
194 },
195);
196
197test.provider.skipIf(!hasHetznerCreds)(
198 "list enumerates the deployed network",
199 (stack) =>
200 Effect.gen(function* () {
201 yield* stack.destroy();
202
203 const deployed = yield* stack.deploy(
204 Effect.gen(function* () {
205 return yield* Hetzner.Network("ListVpc", {
206 ipRange: "10.20.0.0/16",
207 subnets: [
208 {
209 type: "cloud",
210 ipRange: "10.20.1.0/24",
211 networkZone: "eu-central",
212 },
213 ],
214 });
215 }),
216 );
217
218 const provider = yield* Provider.findProvider(Hetzner.Network);
219 const all = yield* provider.list();
220 const found = all.find((network) => network.networkId === deployed.networkId);
221 expect(found).toBeDefined();
222 expect(found?.ipRange).toEqual("10.20.0.0/16");
223 expect(found?.name).toEqual(deployed.name);
224
225 yield* stack.destroy();
226
227 const gone = yield* waitUntilGone(deployed.networkId);
228 expect(gone).toEqual("gone");
229 }).pipe(logLevel),
230 {
231 tags: ["provider:hetzner", "provider:hetzner:network", "provider:hetzner:service", "live"],
232 timeout: 120_000,
233 },
234);