@absolutejs/deploy 0.20.0 → 0.21.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/dist/cloudTarget.d.ts +4 -4
- package/dist/cloudflare.d.ts +2 -2
- package/dist/cloudflare.js.map +2 -2
- package/dist/deployer.d.ts +9 -9
- package/dist/digitalocean.d.ts +5 -5
- package/dist/digitalocean.js +2 -8
- package/dist/digitalocean.js.map +5 -5
- package/dist/digitaloceanDns.d.ts +2 -2
- package/dist/digitaloceanDns.js +2 -8
- package/dist/digitaloceanDns.js.map +6 -6
- package/dist/digitaloceanEphemeralInfrastructure.d.ts +20 -0
- package/dist/digitaloceanEphemeralInfrastructure.js +610 -0
- package/dist/digitaloceanEphemeralInfrastructure.js.map +13 -0
- package/dist/digitaloceanInfrastructure.js +2 -8
- package/dist/digitaloceanInfrastructure.js.map +6 -6
- package/dist/digitaloceanIngress.js +2 -8
- package/dist/digitaloceanIngress.js.map +5 -5
- package/dist/dns.d.ts +1 -1
- package/dist/dns.js.map +2 -2
- package/dist/env.d.ts +1 -1
- package/dist/env.js.map +2 -2
- package/dist/ephemeralInfrastructure.d.ts +37 -0
- package/dist/ephemeralInfrastructure.js +4 -0
- package/dist/ephemeralInfrastructure.js.map +9 -0
- package/dist/gcp.js +2 -6
- package/dist/gcp.js.map +3 -3
- package/dist/hetzner.d.ts +3 -3
- package/dist/hetzner.js +2 -8
- package/dist/hetzner.js.map +5 -5
- package/dist/hetznerDns.d.ts +2 -2
- package/dist/hetznerDns.js.map +2 -2
- package/dist/hetznerInfrastructure.js +2 -8
- package/dist/hetznerInfrastructure.js.map +7 -7
- package/dist/index.d.ts +10 -10
- package/dist/index.js +16 -64
- package/dist/index.js.map +5 -5
- package/dist/linode.d.ts +3 -3
- package/dist/linode.js +2 -8
- package/dist/linode.js.map +5 -5
- package/dist/linodeInfrastructure.js +3 -13
- package/dist/linodeInfrastructure.js.map +7 -7
- package/dist/preview.d.ts +3 -3
- package/dist/preview.js +2 -5
- package/dist/preview.js.map +3 -3
- package/dist/processManagers.d.ts +4 -4
- package/dist/route53.d.ts +2 -2
- package/dist/route53.js.map +2 -2
- package/dist/targets.d.ts +1 -1
- package/dist/tls.d.ts +5 -5
- package/dist/tls.js +2 -5
- package/dist/tls.js.map +3 -3
- package/dist/vultr.d.ts +4 -4
- package/dist/vultr.js +2 -8
- package/dist/vultr.js.map +5 -5
- package/dist/vultrInfrastructure.js +2 -8
- package/dist/vultrInfrastructure.js.map +7 -7
- package/package.json +12 -5
|
@@ -0,0 +1,610 @@
|
|
|
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/digitalocean.ts
|
|
308
|
+
var DO_API_BASE = "https://api.digitalocean.com/v2";
|
|
309
|
+
|
|
310
|
+
class DigitalOceanError extends Error {
|
|
311
|
+
status;
|
|
312
|
+
body;
|
|
313
|
+
constructor(message, status, body) {
|
|
314
|
+
super(message);
|
|
315
|
+
this.name = "DigitalOceanError";
|
|
316
|
+
this.status = status;
|
|
317
|
+
this.body = body;
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
var createDigitalOceanClient = (token, options = {}) => {
|
|
321
|
+
const base = options.baseUrl ?? DO_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 DigitalOceanError(`DigitalOcean 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 createDigitalOceanClient(options.token);
|
|
351
|
+
}
|
|
352
|
+
throw new Error("[deploy/digitalocean] either `token` or `client` must be provided");
|
|
353
|
+
};
|
|
354
|
+
var publicIpv4 = (droplet) => droplet.networks.v4.find((net) => net.type === "public")?.ip_address;
|
|
355
|
+
var findDigitalOceanDroplet = async (client, name) => {
|
|
356
|
+
const body = await client.request("GET", `/droplets?name=${encodeURIComponent(name)}`);
|
|
357
|
+
const matches = body.droplets.filter((droplet) => droplet.name === name);
|
|
358
|
+
if (matches.length === 0)
|
|
359
|
+
return;
|
|
360
|
+
if (matches.length > 1) {
|
|
361
|
+
throw new Error(`[deploy/digitalocean] multiple droplets named "${name}" (${matches.map((droplet) => droplet.id).join(", ")}). Resolve manually before adopting.`);
|
|
362
|
+
}
|
|
363
|
+
return matches[0];
|
|
364
|
+
};
|
|
365
|
+
var listDigitalOceanDroplets = async (options) => {
|
|
366
|
+
const client = resolveClient(options);
|
|
367
|
+
const path = options.tag !== undefined ? `/droplets?tag_name=${encodeURIComponent(options.tag)}` : "/droplets";
|
|
368
|
+
const body = await client.request("GET", path);
|
|
369
|
+
return body.droplets;
|
|
370
|
+
};
|
|
371
|
+
var destroyDigitalOceanDroplet = async (options) => {
|
|
372
|
+
const client = resolveClient(options);
|
|
373
|
+
try {
|
|
374
|
+
await client.request("DELETE", `/droplets/${options.id}`);
|
|
375
|
+
} catch (error) {
|
|
376
|
+
if (error instanceof DigitalOceanError && error.status === 404) {
|
|
377
|
+
return;
|
|
378
|
+
}
|
|
379
|
+
throw error;
|
|
380
|
+
}
|
|
381
|
+
};
|
|
382
|
+
var digitalOceanTarget = async (options) => {
|
|
383
|
+
const client = resolveClient(options);
|
|
384
|
+
const hooks = {
|
|
385
|
+
create: async () => {
|
|
386
|
+
const created = await client.request("POST", "/droplets", {
|
|
387
|
+
name: options.name,
|
|
388
|
+
region: options.region,
|
|
389
|
+
size: options.size,
|
|
390
|
+
image: options.image,
|
|
391
|
+
ssh_keys: [...options.sshKeys],
|
|
392
|
+
...options.tags !== undefined ? { tags: [...options.tags] } : {},
|
|
393
|
+
...options.userData !== undefined ? { user_data: options.userData } : {},
|
|
394
|
+
...options.vpcUuid !== undefined ? { vpc_uuid: options.vpcUuid } : {},
|
|
395
|
+
...options.ipv6 === true ? { ipv6: true } : {},
|
|
396
|
+
...options.monitoring === true ? { monitoring: true } : {}
|
|
397
|
+
});
|
|
398
|
+
return created.droplet;
|
|
399
|
+
},
|
|
400
|
+
destroy: (id) => destroyDigitalOceanDroplet({ client, id }),
|
|
401
|
+
fetch: async (id) => {
|
|
402
|
+
const refreshed = await client.request("GET", `/droplets/${id}`);
|
|
403
|
+
return refreshed.droplet;
|
|
404
|
+
},
|
|
405
|
+
findByName: (name) => findDigitalOceanDroplet(client, name),
|
|
406
|
+
getId: (droplet) => droplet.id,
|
|
407
|
+
getIpv4: publicIpv4,
|
|
408
|
+
getStatus: (droplet) => droplet.status,
|
|
409
|
+
isReady: (droplet) => droplet.status === "active"
|
|
410
|
+
};
|
|
411
|
+
const result = await createCloudTarget(hooks, {
|
|
412
|
+
describeTarget: (sshDescription) => `digitalocean droplet "${options.name}" (${sshDescription})`,
|
|
413
|
+
entityWord: "droplet",
|
|
414
|
+
logPrefix: "[do]",
|
|
415
|
+
name: options.name,
|
|
416
|
+
region: options.region,
|
|
417
|
+
...options.user !== undefined ? { user: options.user } : {},
|
|
418
|
+
...options.identity !== undefined ? { identity: options.identity } : {},
|
|
419
|
+
...options.port !== undefined ? { port: options.port } : {},
|
|
420
|
+
...options.provisionTimeoutMs !== undefined ? { provisionTimeoutMs: options.provisionTimeoutMs } : {},
|
|
421
|
+
...options.sshReadinessTimeoutMs !== undefined ? { sshReadinessTimeoutMs: options.sshReadinessTimeoutMs } : {},
|
|
422
|
+
...options.pollIntervalMs !== undefined ? { pollIntervalMs: options.pollIntervalMs } : {},
|
|
423
|
+
...options.onLog !== undefined ? { onLog: options.onLog } : {},
|
|
424
|
+
...options.probeSsh !== undefined ? { probeSsh: options.probeSsh } : {},
|
|
425
|
+
...options.sleep !== undefined ? { sleep: options.sleep } : {},
|
|
426
|
+
...options.now !== undefined ? { now: options.now } : {}
|
|
427
|
+
});
|
|
428
|
+
return {
|
|
429
|
+
description: result.description,
|
|
430
|
+
destroy: result.destroy,
|
|
431
|
+
dropletId: result.id,
|
|
432
|
+
exec: result.exec,
|
|
433
|
+
ipv4: result.ipv4,
|
|
434
|
+
upload: result.upload,
|
|
435
|
+
...result.close !== undefined ? { close: result.close } : {}
|
|
436
|
+
};
|
|
437
|
+
};
|
|
438
|
+
|
|
439
|
+
// src/digitaloceanEphemeralInfrastructure.ts
|
|
440
|
+
var DEFAULT_CLEANUP_POLL_INTERVAL_MS = 2000;
|
|
441
|
+
var DEFAULT_CLEANUP_TIMEOUT_MS = 120000;
|
|
442
|
+
var MINIMUM_VOLUME_GIB = 1;
|
|
443
|
+
var SAFE_RESOURCE_NAME = /^[a-z0-9][a-z0-9-]{0,62}$/;
|
|
444
|
+
var resolveClient2 = (options) => {
|
|
445
|
+
if (options.client)
|
|
446
|
+
return options.client;
|
|
447
|
+
if (options.token)
|
|
448
|
+
return createDigitalOceanClient(options.token);
|
|
449
|
+
throw new Error("[deploy/digitalocean-ephemeral] either `token` or `client` must be provided");
|
|
450
|
+
};
|
|
451
|
+
var parseDropletId = (id) => {
|
|
452
|
+
const match = /^digitalocean:droplet:([1-9][0-9]*)$/.exec(id);
|
|
453
|
+
if (!match?.[1])
|
|
454
|
+
throw new Error("[deploy/digitalocean-ephemeral] invalid droplet id");
|
|
455
|
+
return Number(match[1]);
|
|
456
|
+
};
|
|
457
|
+
var parseVolumeId = (id) => {
|
|
458
|
+
const match = /^digitalocean:volume:([0-9a-f-]{36})$/.exec(id);
|
|
459
|
+
if (!match?.[1])
|
|
460
|
+
throw new Error("[deploy/digitalocean-ephemeral] invalid volume id");
|
|
461
|
+
return match[1];
|
|
462
|
+
};
|
|
463
|
+
var privateIpv4 = (droplet) => droplet.networks.v4.find(({ type }) => type === "private")?.ip_address;
|
|
464
|
+
var publicIpv42 = (droplet) => droplet.networks.v4.find(({ type }) => type === "public")?.ip_address;
|
|
465
|
+
var normalize = (droplet, volume) => {
|
|
466
|
+
const privateAddress = privateIpv4(droplet);
|
|
467
|
+
const publicAddress = publicIpv42(droplet);
|
|
468
|
+
return {
|
|
469
|
+
node: {
|
|
470
|
+
id: `digitalocean:droplet:${droplet.id}`,
|
|
471
|
+
label: droplet.name,
|
|
472
|
+
provider: "digitalocean",
|
|
473
|
+
region: droplet.region?.slug ?? "unknown",
|
|
474
|
+
state: droplet.status === "active" ? "ready" : droplet.status === "new" ? "pending" : "terminated",
|
|
475
|
+
...privateAddress ? { privateIpv4: privateAddress } : {},
|
|
476
|
+
...publicAddress ? { publicIpv4: publicAddress } : {}
|
|
477
|
+
},
|
|
478
|
+
volume: {
|
|
479
|
+
encryptedAtRest: true,
|
|
480
|
+
id: `digitalocean:volume:${volume.id}`,
|
|
481
|
+
label: volume.name,
|
|
482
|
+
region: volume.region.slug,
|
|
483
|
+
sizeGiB: volume.size_gigabytes
|
|
484
|
+
}
|
|
485
|
+
};
|
|
486
|
+
};
|
|
487
|
+
var findVolume = async (client, name) => {
|
|
488
|
+
const result = await client.request("GET", `/volumes?name=${encodeURIComponent(name)}`);
|
|
489
|
+
const matches = result.volumes.filter((volume) => volume.name === name);
|
|
490
|
+
if (matches.length > 1)
|
|
491
|
+
throw new Error(`[deploy/digitalocean-ephemeral] multiple volumes named "${name}"`);
|
|
492
|
+
return matches[0];
|
|
493
|
+
};
|
|
494
|
+
var absent = async (operation) => {
|
|
495
|
+
try {
|
|
496
|
+
await operation();
|
|
497
|
+
return false;
|
|
498
|
+
} catch (error) {
|
|
499
|
+
if (error instanceof DigitalOceanError && error.status === 404)
|
|
500
|
+
return true;
|
|
501
|
+
throw error;
|
|
502
|
+
}
|
|
503
|
+
};
|
|
504
|
+
var validateRequest = (request) => {
|
|
505
|
+
if (!SAFE_RESOURCE_NAME.test(request.name))
|
|
506
|
+
throw new Error("[deploy/digitalocean-ephemeral] invalid resource name");
|
|
507
|
+
if (!request.idempotencyKey)
|
|
508
|
+
throw new Error("[deploy/digitalocean-ephemeral] idempotency key is required");
|
|
509
|
+
if (!request.vpcUuid)
|
|
510
|
+
throw new Error("[deploy/digitalocean-ephemeral] VPC UUID is required");
|
|
511
|
+
if (request.sshKeys.length === 0)
|
|
512
|
+
throw new Error("[deploy/digitalocean-ephemeral] at least one SSH key is required");
|
|
513
|
+
if (!Number.isSafeInteger(request.volumeGiB) || request.volumeGiB < MINIMUM_VOLUME_GIB)
|
|
514
|
+
throw new Error("[deploy/digitalocean-ephemeral] invalid volume size");
|
|
515
|
+
};
|
|
516
|
+
var createDigitalOceanEphemeralInfrastructureProvider = (options) => {
|
|
517
|
+
if (!options.tag)
|
|
518
|
+
throw new Error("[deploy/digitalocean-ephemeral] a resource tag is required");
|
|
519
|
+
const client = resolveClient2(options);
|
|
520
|
+
const sleep = options.sleep ?? ((milliseconds) => Bun.sleep(milliseconds));
|
|
521
|
+
const pollIntervalMs = options.cleanupPollIntervalMs ?? DEFAULT_CLEANUP_POLL_INTERVAL_MS;
|
|
522
|
+
const cleanupTimeoutMs = options.cleanupTimeoutMs ?? DEFAULT_CLEANUP_TIMEOUT_MS;
|
|
523
|
+
const inspectAbsence = async (resources) => {
|
|
524
|
+
const dropletId = parseDropletId(resources.node.id);
|
|
525
|
+
const volumeId = parseVolumeId(resources.volume.id);
|
|
526
|
+
const [dropletAbsent, volumeAbsent] = await Promise.all([
|
|
527
|
+
absent(() => client.request("GET", `/droplets/${dropletId}`)),
|
|
528
|
+
absent(() => client.request("GET", `/volumes/${volumeId}`))
|
|
529
|
+
]);
|
|
530
|
+
return { dropletAbsent, volumeAbsent };
|
|
531
|
+
};
|
|
532
|
+
const waitUntil = async (predicate, label) => {
|
|
533
|
+
const deadline = Date.now() + cleanupTimeoutMs;
|
|
534
|
+
while (!await predicate()) {
|
|
535
|
+
if (Date.now() >= deadline)
|
|
536
|
+
throw new Error(`[deploy/digitalocean-ephemeral] timed out waiting for ${label}`);
|
|
537
|
+
await sleep(pollIntervalMs);
|
|
538
|
+
}
|
|
539
|
+
};
|
|
540
|
+
return {
|
|
541
|
+
cleanup: async (resources) => {
|
|
542
|
+
const dropletId = parseDropletId(resources.node.id);
|
|
543
|
+
const volumeId = parseVolumeId(resources.volume.id);
|
|
544
|
+
if (!(await inspectAbsence(resources)).dropletAbsent)
|
|
545
|
+
await client.request("DELETE", `/droplets/${dropletId}`);
|
|
546
|
+
await waitUntil(async () => (await inspectAbsence(resources)).dropletAbsent, "droplet deletion");
|
|
547
|
+
if (!(await inspectAbsence(resources)).volumeAbsent)
|
|
548
|
+
await client.request("DELETE", `/volumes/${volumeId}`);
|
|
549
|
+
await waitUntil(async () => (await inspectAbsence(resources)).volumeAbsent, "volume deletion");
|
|
550
|
+
return inspectAbsence(resources);
|
|
551
|
+
},
|
|
552
|
+
inspectAbsence,
|
|
553
|
+
name: "digitalocean",
|
|
554
|
+
provision: async (request) => {
|
|
555
|
+
validateRequest(request);
|
|
556
|
+
const volumeName = `${request.name}-checkpoint`;
|
|
557
|
+
let volume = await findVolume(client, volumeName);
|
|
558
|
+
const createdVolume = !volume;
|
|
559
|
+
if (!volume) {
|
|
560
|
+
const result = await client.request("POST", "/volumes", {
|
|
561
|
+
description: `Ephemeral ${request.name} checkpoint volume`,
|
|
562
|
+
filesystem_label: "criu-checkpoint",
|
|
563
|
+
filesystem_type: "ext4",
|
|
564
|
+
name: volumeName,
|
|
565
|
+
region: request.region,
|
|
566
|
+
size_gigabytes: request.volumeGiB,
|
|
567
|
+
tags: [options.tag]
|
|
568
|
+
});
|
|
569
|
+
volume = result.volume;
|
|
570
|
+
}
|
|
571
|
+
if (volume.region.slug !== request.region || volume.size_gigabytes !== request.volumeGiB)
|
|
572
|
+
throw new Error("[deploy/digitalocean-ephemeral] existing volume does not match the requested topology");
|
|
573
|
+
try {
|
|
574
|
+
let droplet = await findDigitalOceanDroplet(client, request.name);
|
|
575
|
+
if (!droplet) {
|
|
576
|
+
const result = await client.request("POST", "/droplets", {
|
|
577
|
+
backups: false,
|
|
578
|
+
image: request.image,
|
|
579
|
+
ipv6: false,
|
|
580
|
+
monitoring: true,
|
|
581
|
+
name: request.name,
|
|
582
|
+
region: request.region,
|
|
583
|
+
size: request.size,
|
|
584
|
+
ssh_keys: [...request.sshKeys],
|
|
585
|
+
tags: [options.tag],
|
|
586
|
+
user_data: request.userData,
|
|
587
|
+
volumes: [volume.id],
|
|
588
|
+
vpc_uuid: request.vpcUuid
|
|
589
|
+
});
|
|
590
|
+
droplet = result.droplet;
|
|
591
|
+
}
|
|
592
|
+
if (droplet.region?.slug !== request.region || droplet.size_slug && droplet.size_slug !== request.size)
|
|
593
|
+
throw new Error("[deploy/digitalocean-ephemeral] existing droplet does not match the requested topology");
|
|
594
|
+
if (volume.droplet_ids.length > 0 && !volume.droplet_ids.includes(droplet.id))
|
|
595
|
+
throw new Error("[deploy/digitalocean-ephemeral] checkpoint volume is attached to another droplet");
|
|
596
|
+
return normalize(droplet, volume);
|
|
597
|
+
} catch (error) {
|
|
598
|
+
if (createdVolume)
|
|
599
|
+
await client.request("DELETE", `/volumes/${volume.id}`);
|
|
600
|
+
throw error;
|
|
601
|
+
}
|
|
602
|
+
}
|
|
603
|
+
};
|
|
604
|
+
};
|
|
605
|
+
export {
|
|
606
|
+
createDigitalOceanEphemeralInfrastructureProvider
|
|
607
|
+
};
|
|
608
|
+
|
|
609
|
+
//# debugId=B8BFAC3CF04D69D764756E2164756E21
|
|
610
|
+
//# sourceMappingURL=digitaloceanEphemeralInfrastructure.js.map
|