@absolutejs/deploy 0.14.1 → 0.15.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +12 -6
- package/dist/hetzner.d.ts +4 -0
- package/dist/hetzner.js.map +2 -2
- package/dist/hetznerInfrastructure.d.ts +20 -0
- package/dist/hetznerInfrastructure.js +557 -0
- package/dist/hetznerInfrastructure.js.map +14 -0
- package/dist/infrastructureAdapter.d.ts +14 -0
- package/dist/linodeInfrastructure.d.ts +18 -0
- package/dist/linodeInfrastructure.js +573 -0
- package/dist/linodeInfrastructure.js.map +14 -0
- package/dist/vultrInfrastructure.d.ts +18 -0
- package/dist/vultrInfrastructure.js +552 -0
- package/dist/vultrInfrastructure.js.map +14 -0
- package/package.json +17 -2
|
@@ -0,0 +1,557 @@
|
|
|
1
|
+
// @bun
|
|
2
|
+
var __create = Object.create;
|
|
3
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
4
|
+
var __defProp = Object.defineProperty;
|
|
5
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
7
|
+
function __accessProp(key) {
|
|
8
|
+
return this[key];
|
|
9
|
+
}
|
|
10
|
+
var __toESMCache_node;
|
|
11
|
+
var __toESMCache_esm;
|
|
12
|
+
var __toESM = (mod, isNodeMode, target) => {
|
|
13
|
+
var canCache = mod != null && typeof mod === "object";
|
|
14
|
+
if (canCache) {
|
|
15
|
+
var cache = isNodeMode ? __toESMCache_node ??= new WeakMap : __toESMCache_esm ??= new WeakMap;
|
|
16
|
+
var cached = cache.get(mod);
|
|
17
|
+
if (cached)
|
|
18
|
+
return cached;
|
|
19
|
+
}
|
|
20
|
+
target = mod != null ? __create(__getProtoOf(mod)) : {};
|
|
21
|
+
const to = isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target;
|
|
22
|
+
for (let key of __getOwnPropNames(mod))
|
|
23
|
+
if (!__hasOwnProp.call(to, key))
|
|
24
|
+
__defProp(to, key, {
|
|
25
|
+
get: __accessProp.bind(mod, key),
|
|
26
|
+
enumerable: true
|
|
27
|
+
});
|
|
28
|
+
if (canCache)
|
|
29
|
+
cache.set(mod, to);
|
|
30
|
+
return to;
|
|
31
|
+
};
|
|
32
|
+
var __commonJS = (cb, mod) => () => (mod || cb((mod = { exports: {} }).exports, mod), mod.exports);
|
|
33
|
+
var __require = import.meta.require;
|
|
34
|
+
|
|
35
|
+
// src/targets.ts
|
|
36
|
+
import { mkdir } from "fs/promises";
|
|
37
|
+
import { join } from "path";
|
|
38
|
+
var decodeChunks = async (reader, onLine) => {
|
|
39
|
+
if (!reader)
|
|
40
|
+
return "";
|
|
41
|
+
const decoder = new TextDecoder;
|
|
42
|
+
let buffer = "";
|
|
43
|
+
let collected = "";
|
|
44
|
+
const stream = reader.getReader();
|
|
45
|
+
try {
|
|
46
|
+
while (true) {
|
|
47
|
+
const { done, value } = await stream.read();
|
|
48
|
+
if (done)
|
|
49
|
+
break;
|
|
50
|
+
const chunk = decoder.decode(value, { stream: true });
|
|
51
|
+
collected += chunk;
|
|
52
|
+
if (!onLine)
|
|
53
|
+
continue;
|
|
54
|
+
buffer += chunk;
|
|
55
|
+
let newline = buffer.indexOf(`
|
|
56
|
+
`);
|
|
57
|
+
while (newline !== -1) {
|
|
58
|
+
const line = buffer.slice(0, newline).replace(/\r$/, "");
|
|
59
|
+
if (line.length > 0)
|
|
60
|
+
onLine(line);
|
|
61
|
+
buffer = buffer.slice(newline + 1);
|
|
62
|
+
newline = buffer.indexOf(`
|
|
63
|
+
`);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
const tail = decoder.decode();
|
|
67
|
+
collected += tail;
|
|
68
|
+
if (onLine && (buffer + tail).length > 0)
|
|
69
|
+
onLine((buffer + tail).replace(/\r$/, ""));
|
|
70
|
+
} finally {
|
|
71
|
+
stream.releaseLock();
|
|
72
|
+
}
|
|
73
|
+
return collected;
|
|
74
|
+
};
|
|
75
|
+
var runSpawn = async (argv, options) => {
|
|
76
|
+
const proc = Bun.spawn(argv, {
|
|
77
|
+
cwd: options.cwd,
|
|
78
|
+
env: options.env,
|
|
79
|
+
stderr: "pipe",
|
|
80
|
+
stdin: options.stdin === undefined ? "ignore" : "pipe",
|
|
81
|
+
stdout: "pipe"
|
|
82
|
+
});
|
|
83
|
+
if (options.stdin !== undefined && proc.stdin) {
|
|
84
|
+
const sink = proc.stdin;
|
|
85
|
+
const wrote = sink.write(options.stdin);
|
|
86
|
+
if (wrote && typeof wrote.then === "function") {
|
|
87
|
+
await wrote;
|
|
88
|
+
}
|
|
89
|
+
const ended = sink.end();
|
|
90
|
+
if (ended && typeof ended.then === "function") {
|
|
91
|
+
await ended;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
const timeout = options.timeoutMs ?? 600000;
|
|
95
|
+
let timer;
|
|
96
|
+
if (timeout > 0) {
|
|
97
|
+
timer = setTimeout(() => {
|
|
98
|
+
try {
|
|
99
|
+
proc.kill();
|
|
100
|
+
} catch {}
|
|
101
|
+
}, timeout);
|
|
102
|
+
}
|
|
103
|
+
const stdoutPromise = decodeChunks(proc.stdout, options.onLog ? (line) => options.onLog(line, "stdout") : undefined);
|
|
104
|
+
const stderrPromise = decodeChunks(proc.stderr, options.onLog ? (line) => options.onLog(line, "stderr") : undefined);
|
|
105
|
+
const [stdout, stderr, exitCode] = await Promise.all([
|
|
106
|
+
stdoutPromise,
|
|
107
|
+
stderrPromise,
|
|
108
|
+
proc.exited
|
|
109
|
+
]);
|
|
110
|
+
if (timer)
|
|
111
|
+
clearTimeout(timer);
|
|
112
|
+
return { exitCode: exitCode ?? -1, stderr, stdout };
|
|
113
|
+
};
|
|
114
|
+
var localTarget = (options) => {
|
|
115
|
+
const baseEnv = { ...options.env };
|
|
116
|
+
const ensureRoot = async () => {
|
|
117
|
+
await mkdir(options.root, { recursive: true });
|
|
118
|
+
};
|
|
119
|
+
return {
|
|
120
|
+
description: `local ${options.root}`,
|
|
121
|
+
exec: async (cmd, opts) => {
|
|
122
|
+
await ensureRoot();
|
|
123
|
+
return runSpawn(["sh", "-c", cmd], {
|
|
124
|
+
cwd: opts?.cwd ?? options.root,
|
|
125
|
+
env: { ...process.env, ...baseEnv, ...opts?.env ?? {} },
|
|
126
|
+
onLog: opts?.onLog,
|
|
127
|
+
stdin: opts?.stdin,
|
|
128
|
+
timeoutMs: opts?.timeoutMs
|
|
129
|
+
});
|
|
130
|
+
},
|
|
131
|
+
upload: async (localPath, remotePath, opts) => {
|
|
132
|
+
await ensureRoot();
|
|
133
|
+
const dest = remotePath.startsWith("/") ? remotePath : join(options.root, remotePath);
|
|
134
|
+
const argv = ["rsync", "-a"];
|
|
135
|
+
if (opts?.deleteOrphans)
|
|
136
|
+
argv.push("--delete");
|
|
137
|
+
for (const pattern of opts?.exclude ?? [])
|
|
138
|
+
argv.push("--exclude", pattern);
|
|
139
|
+
argv.push(localPath, dest);
|
|
140
|
+
const result = await runSpawn(argv, { timeoutMs: 600000 });
|
|
141
|
+
if (result.exitCode !== 0) {
|
|
142
|
+
throw new Error(`local upload failed (exit ${result.exitCode}): ${result.stderr || result.stdout}`);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
};
|
|
146
|
+
};
|
|
147
|
+
var sshTargetString = (options) => {
|
|
148
|
+
const user = options.user ?? "root";
|
|
149
|
+
return `${user}@${options.host}`;
|
|
150
|
+
};
|
|
151
|
+
var sshBaseFlags = (options) => {
|
|
152
|
+
const flags = [];
|
|
153
|
+
if (options.port !== undefined && options.port !== 22)
|
|
154
|
+
flags.push("-p", String(options.port));
|
|
155
|
+
if (options.identity !== undefined)
|
|
156
|
+
flags.push("-i", options.identity);
|
|
157
|
+
flags.push("-o", "BatchMode=yes", "-o", "StrictHostKeyChecking=accept-new");
|
|
158
|
+
for (const flag of options.sshFlags ?? [])
|
|
159
|
+
flags.push(flag);
|
|
160
|
+
return flags;
|
|
161
|
+
};
|
|
162
|
+
var shellQuote = (value) => `'${value.replace(/'/g, `'\\''`)}'`;
|
|
163
|
+
var buildRemoteCmd = (cmd, opts) => {
|
|
164
|
+
const env = opts?.env;
|
|
165
|
+
const envPrefix = env ? Object.entries(env).map(([k, v]) => `${k}=${shellQuote(v)}`).join(" ") + " " : "";
|
|
166
|
+
if (opts?.cwd) {
|
|
167
|
+
return `cd ${shellQuote(opts.cwd)} && ${envPrefix}${cmd}`;
|
|
168
|
+
}
|
|
169
|
+
return `${envPrefix}${cmd}`;
|
|
170
|
+
};
|
|
171
|
+
var sshTarget = (options) => {
|
|
172
|
+
const remote = sshTargetString(options);
|
|
173
|
+
const useRsync = options.rsync ?? true;
|
|
174
|
+
return {
|
|
175
|
+
description: `ssh ${remote}${options.port && options.port !== 22 ? `:${options.port}` : ""}`,
|
|
176
|
+
exec: async (cmd, opts) => {
|
|
177
|
+
const argv = ["ssh", ...sshBaseFlags(options)];
|
|
178
|
+
for (const name of options.forwardEnv ?? [])
|
|
179
|
+
argv.push("-o", `SendEnv=${name}`);
|
|
180
|
+
argv.push(remote, buildRemoteCmd(cmd, opts));
|
|
181
|
+
return runSpawn(argv, {
|
|
182
|
+
onLog: opts?.onLog,
|
|
183
|
+
stdin: opts?.stdin,
|
|
184
|
+
timeoutMs: opts?.timeoutMs
|
|
185
|
+
});
|
|
186
|
+
},
|
|
187
|
+
upload: async (localPath, remotePath, opts) => {
|
|
188
|
+
if (useRsync) {
|
|
189
|
+
const sshCmd = ["ssh", ...sshBaseFlags(options)].map((part) => part.includes(" ") ? `'${part}'` : part).join(" ");
|
|
190
|
+
const argv2 = ["rsync", "-az", "-e", sshCmd];
|
|
191
|
+
if (opts?.deleteOrphans)
|
|
192
|
+
argv2.push("--delete");
|
|
193
|
+
for (const pattern of opts?.exclude ?? [])
|
|
194
|
+
argv2.push("--exclude", pattern);
|
|
195
|
+
argv2.push(localPath, `${remote}:${remotePath}`);
|
|
196
|
+
const result2 = await runSpawn(argv2, { timeoutMs: 600000 });
|
|
197
|
+
if (result2.exitCode !== 0) {
|
|
198
|
+
throw new Error(`rsync upload failed (exit ${result2.exitCode}): ${result2.stderr || result2.stdout}`);
|
|
199
|
+
}
|
|
200
|
+
return;
|
|
201
|
+
}
|
|
202
|
+
const argv = ["scp", "-r", ...sshBaseFlags(options), localPath, `${remote}:${remotePath}`];
|
|
203
|
+
const result = await runSpawn(argv, { timeoutMs: 600000 });
|
|
204
|
+
if (result.exitCode !== 0) {
|
|
205
|
+
throw new Error(`scp upload failed (exit ${result.exitCode}): ${result.stderr || result.stdout}`);
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
};
|
|
209
|
+
};
|
|
210
|
+
|
|
211
|
+
// src/cloudTarget.ts
|
|
212
|
+
var defaultSleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
213
|
+
var defaultProbeSsh = async (host, port) => {
|
|
214
|
+
const PROBE_TIMEOUT_MS = 2000;
|
|
215
|
+
return new Promise((resolve) => {
|
|
216
|
+
let settled = false;
|
|
217
|
+
const settle = (value) => {
|
|
218
|
+
if (settled)
|
|
219
|
+
return;
|
|
220
|
+
settled = true;
|
|
221
|
+
resolve(value);
|
|
222
|
+
};
|
|
223
|
+
const timer = setTimeout(() => settle(false), PROBE_TIMEOUT_MS);
|
|
224
|
+
Bun.connect({
|
|
225
|
+
hostname: host,
|
|
226
|
+
port,
|
|
227
|
+
socket: {
|
|
228
|
+
data: () => {},
|
|
229
|
+
error: () => {
|
|
230
|
+
clearTimeout(timer);
|
|
231
|
+
settle(false);
|
|
232
|
+
},
|
|
233
|
+
open: (socket) => {
|
|
234
|
+
clearTimeout(timer);
|
|
235
|
+
socket.end();
|
|
236
|
+
settle(true);
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
}).catch(() => {
|
|
240
|
+
clearTimeout(timer);
|
|
241
|
+
settle(false);
|
|
242
|
+
});
|
|
243
|
+
});
|
|
244
|
+
};
|
|
245
|
+
var createCloudTarget = async (hooks, options) => {
|
|
246
|
+
const log = options.onLog ?? (() => {});
|
|
247
|
+
const probeSsh = options.probeSsh ?? defaultProbeSsh;
|
|
248
|
+
const sleep = options.sleep ?? defaultSleep;
|
|
249
|
+
const now = options.now ?? Date.now;
|
|
250
|
+
const pollMs = options.pollIntervalMs ?? 5000;
|
|
251
|
+
const provisionTimeout = options.provisionTimeoutMs ?? 5 * 60000;
|
|
252
|
+
const sshTimeout = options.sshReadinessTimeoutMs ?? 2 * 60000;
|
|
253
|
+
const port = options.port ?? 22;
|
|
254
|
+
const prefix = options.logPrefix;
|
|
255
|
+
const noun = options.entityWord;
|
|
256
|
+
const existing = await hooks.findByName(options.name);
|
|
257
|
+
let current;
|
|
258
|
+
if (existing === undefined) {
|
|
259
|
+
log(`${prefix} creating ${noun} "${options.name}" in ${options.region}`);
|
|
260
|
+
current = await hooks.create();
|
|
261
|
+
} else {
|
|
262
|
+
log(`${prefix} reusing ${noun} "${options.name}" (id ${hooks.getId(existing)}, status ${hooks.getStatus(existing)})`);
|
|
263
|
+
current = existing;
|
|
264
|
+
}
|
|
265
|
+
const provisionStart = now();
|
|
266
|
+
let ipv4 = hooks.getIpv4(current);
|
|
267
|
+
while (!hooks.isReady(current) || ipv4 === undefined) {
|
|
268
|
+
if (now() - provisionStart > provisionTimeout) {
|
|
269
|
+
throw new Error(`${prefix} provision timeout after ${provisionTimeout}ms \u2014 ${noun} ${hooks.getId(current)} status "${hooks.getStatus(current)}", ipv4 ${ipv4 ?? "(unassigned)"}`);
|
|
270
|
+
}
|
|
271
|
+
await sleep(pollMs);
|
|
272
|
+
current = await hooks.fetch(hooks.getId(current));
|
|
273
|
+
ipv4 = hooks.getIpv4(current);
|
|
274
|
+
log(`${prefix} poll: status=${hooks.getStatus(current)} ipv4=${ipv4 ?? "(none yet)"}`);
|
|
275
|
+
}
|
|
276
|
+
log(`${prefix} ${noun} ready at ${ipv4}`);
|
|
277
|
+
const sshStart = now();
|
|
278
|
+
while (!await probeSsh(ipv4, port)) {
|
|
279
|
+
if (now() - sshStart > sshTimeout) {
|
|
280
|
+
throw new Error(`${prefix} SSH readiness timeout after ${sshTimeout}ms \u2014 ${ipv4}:${port} did not accept connections`);
|
|
281
|
+
}
|
|
282
|
+
await sleep(pollMs);
|
|
283
|
+
log(`${prefix} waiting on ssh ${ipv4}:${port}`);
|
|
284
|
+
}
|
|
285
|
+
log(`${prefix} ssh ready at ${ipv4}:${port}`);
|
|
286
|
+
const ssh = sshTarget({
|
|
287
|
+
host: ipv4,
|
|
288
|
+
...options.user !== undefined ? { user: options.user } : {},
|
|
289
|
+
...options.identity !== undefined ? { identity: options.identity } : {},
|
|
290
|
+
...options.port !== undefined ? { port: options.port } : {}
|
|
291
|
+
});
|
|
292
|
+
const id = hooks.getId(current);
|
|
293
|
+
const resolvedIpv4 = ipv4;
|
|
294
|
+
return {
|
|
295
|
+
description: options.describeTarget(ssh.description),
|
|
296
|
+
destroy: () => hooks.destroy(id).then(() => {
|
|
297
|
+
log(`${prefix} destroyed ${noun} ${id}`);
|
|
298
|
+
}),
|
|
299
|
+
exec: ssh.exec,
|
|
300
|
+
id,
|
|
301
|
+
ipv4: resolvedIpv4,
|
|
302
|
+
upload: ssh.upload,
|
|
303
|
+
...ssh.close !== undefined ? { close: ssh.close } : {}
|
|
304
|
+
};
|
|
305
|
+
};
|
|
306
|
+
|
|
307
|
+
// src/hetzner.ts
|
|
308
|
+
var HETZNER_API_BASE = "https://api.hetzner.cloud/v1";
|
|
309
|
+
|
|
310
|
+
class HetznerError extends Error {
|
|
311
|
+
status;
|
|
312
|
+
body;
|
|
313
|
+
constructor(message, status, body) {
|
|
314
|
+
super(message);
|
|
315
|
+
this.name = "HetznerError";
|
|
316
|
+
this.status = status;
|
|
317
|
+
this.body = body;
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
var createHetznerClient = (token, options = {}) => {
|
|
321
|
+
const base = options.baseUrl ?? HETZNER_API_BASE;
|
|
322
|
+
const f = options.fetch ?? fetch;
|
|
323
|
+
return {
|
|
324
|
+
request: async (method, path, body) => {
|
|
325
|
+
const init = {
|
|
326
|
+
headers: {
|
|
327
|
+
authorization: `Bearer ${token}`,
|
|
328
|
+
"content-type": "application/json"
|
|
329
|
+
},
|
|
330
|
+
method
|
|
331
|
+
};
|
|
332
|
+
if (body !== undefined)
|
|
333
|
+
init.body = JSON.stringify(body);
|
|
334
|
+
const response = await f(`${base}${path}`, init);
|
|
335
|
+
if (response.status === 204)
|
|
336
|
+
return;
|
|
337
|
+
const text = await response.text();
|
|
338
|
+
const parsed = text.length > 0 ? JSON.parse(text) : undefined;
|
|
339
|
+
if (!response.ok) {
|
|
340
|
+
throw new HetznerError(`Hetzner Cloud API ${method} ${path} failed: ${response.status} ${response.statusText}`, response.status, parsed);
|
|
341
|
+
}
|
|
342
|
+
return parsed;
|
|
343
|
+
}
|
|
344
|
+
};
|
|
345
|
+
};
|
|
346
|
+
var resolveClient = (options) => {
|
|
347
|
+
if (options.client !== undefined)
|
|
348
|
+
return options.client;
|
|
349
|
+
if (options.token !== undefined && options.token.length > 0) {
|
|
350
|
+
return createHetznerClient(options.token);
|
|
351
|
+
}
|
|
352
|
+
throw new Error("[deploy/hetzner] either `token` or `client` must be provided");
|
|
353
|
+
};
|
|
354
|
+
var publicIpv4 = (server) => server.public_net.ipv4?.ip;
|
|
355
|
+
var findHetznerServer = async (client, name) => {
|
|
356
|
+
const body = await client.request("GET", `/servers?name=${encodeURIComponent(name)}`);
|
|
357
|
+
const matches = body.servers.filter((server) => server.name === name);
|
|
358
|
+
if (matches.length === 0)
|
|
359
|
+
return;
|
|
360
|
+
if (matches.length > 1) {
|
|
361
|
+
throw new Error(`[deploy/hetzner] multiple servers named "${name}" (${matches.map((server) => server.id).join(", ")}). Hetzner shouldn't allow this \u2014 resolve manually.`);
|
|
362
|
+
}
|
|
363
|
+
return matches[0];
|
|
364
|
+
};
|
|
365
|
+
var listHetznerServers = async (options) => {
|
|
366
|
+
const client = resolveClient(options);
|
|
367
|
+
const path = options.labelSelector !== undefined ? `/servers?label_selector=${encodeURIComponent(options.labelSelector)}` : "/servers";
|
|
368
|
+
const body = await client.request("GET", path);
|
|
369
|
+
return body.servers;
|
|
370
|
+
};
|
|
371
|
+
var destroyHetznerServer = async (options) => {
|
|
372
|
+
const client = resolveClient(options);
|
|
373
|
+
try {
|
|
374
|
+
await client.request("DELETE", `/servers/${options.id}`);
|
|
375
|
+
} catch (error) {
|
|
376
|
+
if (error instanceof HetznerError && error.status === 404) {
|
|
377
|
+
return;
|
|
378
|
+
}
|
|
379
|
+
throw error;
|
|
380
|
+
}
|
|
381
|
+
};
|
|
382
|
+
var hetznerTarget = async (options) => {
|
|
383
|
+
const client = resolveClient(options);
|
|
384
|
+
const publicIpv4Enabled = options.disablePublicIpv4 !== true;
|
|
385
|
+
const publicIpv6Enabled = options.disablePublicIpv6 !== true;
|
|
386
|
+
const hooks = {
|
|
387
|
+
create: async () => {
|
|
388
|
+
const created = await client.request("POST", "/servers", {
|
|
389
|
+
name: options.name,
|
|
390
|
+
location: options.location,
|
|
391
|
+
server_type: options.serverType,
|
|
392
|
+
image: options.image,
|
|
393
|
+
ssh_keys: [...options.sshKeys],
|
|
394
|
+
start_after_create: true,
|
|
395
|
+
public_net: {
|
|
396
|
+
enable_ipv4: publicIpv4Enabled,
|
|
397
|
+
enable_ipv6: publicIpv6Enabled
|
|
398
|
+
},
|
|
399
|
+
...options.labels !== undefined ? { labels: options.labels } : {},
|
|
400
|
+
...options.userData !== undefined ? { user_data: options.userData } : {},
|
|
401
|
+
...options.networkId !== undefined ? { networks: [options.networkId] } : {}
|
|
402
|
+
});
|
|
403
|
+
return created.server;
|
|
404
|
+
},
|
|
405
|
+
destroy: (id) => destroyHetznerServer({ client, id }),
|
|
406
|
+
fetch: async (id) => {
|
|
407
|
+
const refreshed = await client.request("GET", `/servers/${id}`);
|
|
408
|
+
return refreshed.server;
|
|
409
|
+
},
|
|
410
|
+
findByName: (name) => findHetznerServer(client, name),
|
|
411
|
+
getId: (server) => server.id,
|
|
412
|
+
getIpv4: publicIpv4,
|
|
413
|
+
getStatus: (server) => server.status,
|
|
414
|
+
isReady: (server) => server.status === "running"
|
|
415
|
+
};
|
|
416
|
+
const result = await createCloudTarget(hooks, {
|
|
417
|
+
describeTarget: (sshDescription) => `hetzner server "${options.name}" (${sshDescription})`,
|
|
418
|
+
entityWord: "server",
|
|
419
|
+
logPrefix: "[hetzner]",
|
|
420
|
+
name: options.name,
|
|
421
|
+
region: options.location,
|
|
422
|
+
...options.user !== undefined ? { user: options.user } : {},
|
|
423
|
+
...options.identity !== undefined ? { identity: options.identity } : {},
|
|
424
|
+
...options.port !== undefined ? { port: options.port } : {},
|
|
425
|
+
...options.provisionTimeoutMs !== undefined ? { provisionTimeoutMs: options.provisionTimeoutMs } : {},
|
|
426
|
+
...options.sshReadinessTimeoutMs !== undefined ? { sshReadinessTimeoutMs: options.sshReadinessTimeoutMs } : {},
|
|
427
|
+
...options.pollIntervalMs !== undefined ? { pollIntervalMs: options.pollIntervalMs } : {},
|
|
428
|
+
...options.onLog !== undefined ? { onLog: options.onLog } : {},
|
|
429
|
+
...options.probeSsh !== undefined ? { probeSsh: options.probeSsh } : {},
|
|
430
|
+
...options.sleep !== undefined ? { sleep: options.sleep } : {},
|
|
431
|
+
...options.now !== undefined ? { now: options.now } : {}
|
|
432
|
+
});
|
|
433
|
+
return {
|
|
434
|
+
description: result.description,
|
|
435
|
+
destroy: result.destroy,
|
|
436
|
+
exec: result.exec,
|
|
437
|
+
ipv4: result.ipv4,
|
|
438
|
+
serverId: result.id,
|
|
439
|
+
upload: result.upload,
|
|
440
|
+
...result.close !== undefined ? { close: result.close } : {}
|
|
441
|
+
};
|
|
442
|
+
};
|
|
443
|
+
|
|
444
|
+
// src/infrastructureAdapter.ts
|
|
445
|
+
var infrastructureAgent = (options, addresses) => {
|
|
446
|
+
if (!options)
|
|
447
|
+
return;
|
|
448
|
+
const host = options.preferPrivateNetwork ? addresses.privateIpv4 ?? addresses.publicIpv4 : addresses.publicIpv4 ?? addresses.privateIpv4;
|
|
449
|
+
if (!host)
|
|
450
|
+
return;
|
|
451
|
+
return {
|
|
452
|
+
url: `${options.protocol ?? "http"}://${host}:${options.port ?? 8081}/`,
|
|
453
|
+
...options.audience ? { audience: options.audience } : {}
|
|
454
|
+
};
|
|
455
|
+
};
|
|
456
|
+
var leastPopulatedRegion = (regions, observed, requested) => {
|
|
457
|
+
const eligible = requested ? regions.filter((region) => region.region === requested) : [...regions];
|
|
458
|
+
if (eligible.length === 0)
|
|
459
|
+
return;
|
|
460
|
+
const counts = new Map(eligible.map((region) => [region.region, 0]));
|
|
461
|
+
for (const region of observed) {
|
|
462
|
+
if (counts.has(region))
|
|
463
|
+
counts.set(region, (counts.get(region) ?? 0) + 1);
|
|
464
|
+
}
|
|
465
|
+
const selected = [...counts].sort((left, right) => left[1] - right[1] || left[0].localeCompare(right[0]))[0]?.[0];
|
|
466
|
+
return eligible.find((region) => region.region === selected);
|
|
467
|
+
};
|
|
468
|
+
|
|
469
|
+
// src/hetznerInfrastructure.ts
|
|
470
|
+
var stateFor = (status) => {
|
|
471
|
+
if (status === "running")
|
|
472
|
+
return "ready";
|
|
473
|
+
if (["initializing", "starting", "migrating", "rebuilding"].includes(status))
|
|
474
|
+
return "pending";
|
|
475
|
+
return "terminated";
|
|
476
|
+
};
|
|
477
|
+
var parseNodeId = (id) => {
|
|
478
|
+
const match = /^hetzner:([1-9][0-9]*)$/.exec(id);
|
|
479
|
+
if (!match?.[1])
|
|
480
|
+
throw new Error("[deploy/hetzner] invalid infrastructure node id");
|
|
481
|
+
return Number(match[1]);
|
|
482
|
+
};
|
|
483
|
+
var createHetznerInfrastructureProvider = (options) => {
|
|
484
|
+
if (options.regions.length === 0)
|
|
485
|
+
throw new Error("[deploy/hetzner] at least one fleet region is required");
|
|
486
|
+
const client = options.client ?? (options.token ? createHetznerClient(options.token) : undefined);
|
|
487
|
+
if (!client)
|
|
488
|
+
throw new Error("[deploy/hetzner] either `token` or `client` must be provided");
|
|
489
|
+
const labelKey = options.labelKey ?? "absolutejs-role";
|
|
490
|
+
const labelValue = options.labelValue ?? "absolutejs-paas-node";
|
|
491
|
+
const normalize = (server) => {
|
|
492
|
+
const publicIpv42 = server.public_net.ipv4?.ip;
|
|
493
|
+
const privateIpv4 = server.private_net?.[0]?.ip;
|
|
494
|
+
const agent = infrastructureAgent(options.agent, {
|
|
495
|
+
privateIpv4,
|
|
496
|
+
publicIpv4: publicIpv42
|
|
497
|
+
});
|
|
498
|
+
return {
|
|
499
|
+
id: `hetzner:${server.id}`,
|
|
500
|
+
label: server.name,
|
|
501
|
+
provider: "hetzner",
|
|
502
|
+
region: server.datacenter?.location.name ?? "unknown",
|
|
503
|
+
state: stateFor(server.status),
|
|
504
|
+
...publicIpv42 ? { publicIpv4: publicIpv42 } : {},
|
|
505
|
+
...privateIpv4 ? { privateIpv4 } : {},
|
|
506
|
+
...agent ? { agent } : {}
|
|
507
|
+
};
|
|
508
|
+
};
|
|
509
|
+
const list = () => listHetznerServers({
|
|
510
|
+
client,
|
|
511
|
+
labelSelector: `${labelKey}=${labelValue}`
|
|
512
|
+
});
|
|
513
|
+
return {
|
|
514
|
+
capabilities: {
|
|
515
|
+
cloudInit: true,
|
|
516
|
+
idempotentProvisioning: true,
|
|
517
|
+
privateNetworking: true,
|
|
518
|
+
regionalPlacement: true,
|
|
519
|
+
regions: options.regions.map(({ region }) => region)
|
|
520
|
+
},
|
|
521
|
+
getNode: async (id) => {
|
|
522
|
+
const result = await client.request("GET", `/servers/${parseNodeId(id)}`);
|
|
523
|
+
return normalize(result.server);
|
|
524
|
+
},
|
|
525
|
+
listNodes: async () => (await list()).map(normalize),
|
|
526
|
+
name: "hetzner",
|
|
527
|
+
provisionNode: async (input) => {
|
|
528
|
+
const existing = await findHetznerServer(client, input.name);
|
|
529
|
+
if (existing)
|
|
530
|
+
return normalize(existing);
|
|
531
|
+
const servers = await list();
|
|
532
|
+
const region = leastPopulatedRegion(options.regions, servers.map((server) => server.datacenter?.location.name ?? "unknown"), input.region);
|
|
533
|
+
if (!region)
|
|
534
|
+
throw new Error(`[deploy/hetzner] region ${input.region ?? "(any)"} is not configured`);
|
|
535
|
+
const result = await client.request("POST", "/servers", {
|
|
536
|
+
image: region.image,
|
|
537
|
+
labels: { [labelKey]: labelValue },
|
|
538
|
+
location: region.region,
|
|
539
|
+
name: input.name,
|
|
540
|
+
public_net: { enable_ipv4: true, enable_ipv6: true },
|
|
541
|
+
server_type: region.serverType,
|
|
542
|
+
ssh_keys: [...region.sshKeys],
|
|
543
|
+
start_after_create: true,
|
|
544
|
+
...region.networkId ? { networks: [region.networkId] } : {},
|
|
545
|
+
...region.userData ? { user_data: region.userData } : {}
|
|
546
|
+
});
|
|
547
|
+
return normalize(result.server);
|
|
548
|
+
},
|
|
549
|
+
terminateNode: async (id) => destroyHetznerServer({ client, id: parseNodeId(id) })
|
|
550
|
+
};
|
|
551
|
+
};
|
|
552
|
+
export {
|
|
553
|
+
createHetznerInfrastructureProvider
|
|
554
|
+
};
|
|
555
|
+
|
|
556
|
+
//# debugId=E5D82A7D407A792564756E2164756E21
|
|
557
|
+
//# sourceMappingURL=hetznerInfrastructure.js.map
|