@absolutejs/deploy 0.14.1 → 0.16.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/digitaloceanInfrastructure.js +2 -2
- package/dist/digitaloceanInfrastructure.js.map +3 -3
- package/dist/gcp.js +5 -2
- package/dist/gcp.js.map +3 -3
- 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/infrastructure.d.ts +2 -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,573 @@
|
|
|
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/linode.ts
|
|
308
|
+
var LINODE_API_BASE = "https://api.linode.com/v4";
|
|
309
|
+
|
|
310
|
+
class LinodeError extends Error {
|
|
311
|
+
status;
|
|
312
|
+
body;
|
|
313
|
+
constructor(message, status, body) {
|
|
314
|
+
super(message);
|
|
315
|
+
this.name = "LinodeError";
|
|
316
|
+
this.status = status;
|
|
317
|
+
this.body = body;
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
var createLinodeClient = (token, options = {}) => {
|
|
321
|
+
const base = options.baseUrl ?? LINODE_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 LinodeError(`Linode 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 createLinodeClient(options.token);
|
|
351
|
+
}
|
|
352
|
+
throw new Error("[deploy/linode] either `token` or `client` must be provided");
|
|
353
|
+
};
|
|
354
|
+
var publicIpv4 = (instance) => {
|
|
355
|
+
for (const ip of instance.ipv4) {
|
|
356
|
+
if (!ip.startsWith("10.") && !ip.startsWith("192.168.") && !ip.startsWith("172.") && !ip.startsWith("169.254.")) {
|
|
357
|
+
return ip;
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
return;
|
|
361
|
+
};
|
|
362
|
+
var randomPassword = (length) => {
|
|
363
|
+
const chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789!@#$%^&*";
|
|
364
|
+
const bytes = new Uint8Array(length);
|
|
365
|
+
crypto.getRandomValues(bytes);
|
|
366
|
+
let out = "";
|
|
367
|
+
for (const byte of bytes)
|
|
368
|
+
out += chars[byte % chars.length];
|
|
369
|
+
return out;
|
|
370
|
+
};
|
|
371
|
+
var findLinodeInstance = async (client, name) => {
|
|
372
|
+
const body = await client.request("GET", "/linode/instances?page_size=500");
|
|
373
|
+
const matches = body.data.filter((instance) => instance.label === name);
|
|
374
|
+
if (matches.length === 0)
|
|
375
|
+
return;
|
|
376
|
+
if (matches.length > 1) {
|
|
377
|
+
throw new Error(`[deploy/linode] multiple instances labeled "${name}" (${matches.map((instance) => instance.id).join(", ")}). Resolve manually before adopting.`);
|
|
378
|
+
}
|
|
379
|
+
return matches[0];
|
|
380
|
+
};
|
|
381
|
+
var listLinodeInstances = async (options) => {
|
|
382
|
+
const client = resolveClient(options);
|
|
383
|
+
const path = options.tag !== undefined ? `/linode/instances?tag=${encodeURIComponent(options.tag)}&page_size=500` : "/linode/instances?page_size=500";
|
|
384
|
+
const body = await client.request("GET", path);
|
|
385
|
+
return body.data;
|
|
386
|
+
};
|
|
387
|
+
var destroyLinodeInstance = async (options) => {
|
|
388
|
+
const client = resolveClient(options);
|
|
389
|
+
try {
|
|
390
|
+
await client.request("DELETE", `/linode/instances/${options.id}`);
|
|
391
|
+
} catch (error) {
|
|
392
|
+
if (error instanceof LinodeError && error.status === 404)
|
|
393
|
+
return;
|
|
394
|
+
throw error;
|
|
395
|
+
}
|
|
396
|
+
};
|
|
397
|
+
var linodeTarget = async (options) => {
|
|
398
|
+
const client = resolveClient(options);
|
|
399
|
+
const rootPass = options.rootPass ?? randomPassword(32);
|
|
400
|
+
const hooks = {
|
|
401
|
+
create: async () => {
|
|
402
|
+
const created = await client.request("POST", "/linode/instances", {
|
|
403
|
+
authorized_keys: [...options.sshKeys],
|
|
404
|
+
image: options.image,
|
|
405
|
+
label: options.name,
|
|
406
|
+
region: options.region,
|
|
407
|
+
root_pass: rootPass,
|
|
408
|
+
type: options.type,
|
|
409
|
+
...options.tags !== undefined ? { tags: [...options.tags] } : {},
|
|
410
|
+
...options.userData !== undefined ? { stackscript_data: { user_data: options.userData } } : {},
|
|
411
|
+
...options.privateIp === true ? { private_ip: true } : {},
|
|
412
|
+
...options.backupsEnabled === true ? { backups_enabled: true } : {}
|
|
413
|
+
});
|
|
414
|
+
return created;
|
|
415
|
+
},
|
|
416
|
+
destroy: (id) => destroyLinodeInstance({ client, id }),
|
|
417
|
+
fetch: (id) => client.request("GET", `/linode/instances/${id}`),
|
|
418
|
+
findByName: (name) => findLinodeInstance(client, name),
|
|
419
|
+
getId: (instance) => instance.id,
|
|
420
|
+
getIpv4: publicIpv4,
|
|
421
|
+
getStatus: (instance) => instance.status,
|
|
422
|
+
isReady: (instance) => instance.status === "running"
|
|
423
|
+
};
|
|
424
|
+
const result = await createCloudTarget(hooks, {
|
|
425
|
+
describeTarget: (sshDescription) => `linode "${options.name}" (${sshDescription})`,
|
|
426
|
+
entityWord: "instance",
|
|
427
|
+
logPrefix: "[linode]",
|
|
428
|
+
name: options.name,
|
|
429
|
+
region: options.region,
|
|
430
|
+
...options.user !== undefined ? { user: options.user } : {},
|
|
431
|
+
...options.identity !== undefined ? { identity: options.identity } : {},
|
|
432
|
+
...options.port !== undefined ? { port: options.port } : {},
|
|
433
|
+
...options.provisionTimeoutMs !== undefined ? { provisionTimeoutMs: options.provisionTimeoutMs } : {},
|
|
434
|
+
...options.sshReadinessTimeoutMs !== undefined ? { sshReadinessTimeoutMs: options.sshReadinessTimeoutMs } : {},
|
|
435
|
+
...options.pollIntervalMs !== undefined ? { pollIntervalMs: options.pollIntervalMs } : {},
|
|
436
|
+
...options.onLog !== undefined ? { onLog: options.onLog } : {},
|
|
437
|
+
...options.probeSsh !== undefined ? { probeSsh: options.probeSsh } : {},
|
|
438
|
+
...options.sleep !== undefined ? { sleep: options.sleep } : {},
|
|
439
|
+
...options.now !== undefined ? { now: options.now } : {}
|
|
440
|
+
});
|
|
441
|
+
return {
|
|
442
|
+
description: result.description,
|
|
443
|
+
destroy: result.destroy,
|
|
444
|
+
exec: result.exec,
|
|
445
|
+
instanceId: result.id,
|
|
446
|
+
ipv4: result.ipv4,
|
|
447
|
+
upload: result.upload,
|
|
448
|
+
...result.close !== undefined ? { close: result.close } : {}
|
|
449
|
+
};
|
|
450
|
+
};
|
|
451
|
+
|
|
452
|
+
// src/infrastructureAdapter.ts
|
|
453
|
+
var infrastructureAgent = (options, addresses) => {
|
|
454
|
+
if (!options)
|
|
455
|
+
return;
|
|
456
|
+
const host = options.preferPrivateNetwork ? addresses.privateIpv4 ?? addresses.publicIpv4 : addresses.publicIpv4 ?? addresses.privateIpv4;
|
|
457
|
+
if (!host)
|
|
458
|
+
return;
|
|
459
|
+
return {
|
|
460
|
+
url: `${options.protocol ?? "http"}://${host}:${options.port ?? 8081}/`,
|
|
461
|
+
...options.audience ? { audience: options.audience } : {}
|
|
462
|
+
};
|
|
463
|
+
};
|
|
464
|
+
var leastPopulatedRegion = (regions, observed, requested) => {
|
|
465
|
+
const eligible = requested ? regions.filter((region) => region.region === requested) : [...regions];
|
|
466
|
+
if (eligible.length === 0)
|
|
467
|
+
return;
|
|
468
|
+
const counts = new Map(eligible.map((region) => [region.region, 0]));
|
|
469
|
+
for (const region of observed) {
|
|
470
|
+
if (counts.has(region))
|
|
471
|
+
counts.set(region, (counts.get(region) ?? 0) + 1);
|
|
472
|
+
}
|
|
473
|
+
const selected = [...counts].sort((left, right) => left[1] - right[1] || left[0].localeCompare(right[0]))[0]?.[0];
|
|
474
|
+
return eligible.find((region) => region.region === selected);
|
|
475
|
+
};
|
|
476
|
+
|
|
477
|
+
// src/linodeInfrastructure.ts
|
|
478
|
+
var isPrivate = (ip) => ip.startsWith("10.") || ip.startsWith("192.168.") || ip.startsWith("172.") || ip.startsWith("169.254.");
|
|
479
|
+
var stateFor = (status) => {
|
|
480
|
+
if (status === "running")
|
|
481
|
+
return "ready";
|
|
482
|
+
if ([
|
|
483
|
+
"booting",
|
|
484
|
+
"rebooting",
|
|
485
|
+
"provisioning",
|
|
486
|
+
"migrating",
|
|
487
|
+
"rebuilding",
|
|
488
|
+
"cloning",
|
|
489
|
+
"restoring"
|
|
490
|
+
].includes(status))
|
|
491
|
+
return "pending";
|
|
492
|
+
return "terminated";
|
|
493
|
+
};
|
|
494
|
+
var parseNodeId = (id) => {
|
|
495
|
+
const match = /^linode:([1-9][0-9]*)$/.exec(id);
|
|
496
|
+
if (!match?.[1])
|
|
497
|
+
throw new Error("[deploy/linode] invalid infrastructure node id");
|
|
498
|
+
return Number(match[1]);
|
|
499
|
+
};
|
|
500
|
+
var rootPassword = () => {
|
|
501
|
+
const bytes = crypto.getRandomValues(new Uint8Array(24));
|
|
502
|
+
return Buffer.from(bytes).toString("base64url");
|
|
503
|
+
};
|
|
504
|
+
var createLinodeInfrastructureProvider = (options) => {
|
|
505
|
+
if (options.regions.length === 0)
|
|
506
|
+
throw new Error("[deploy/linode] at least one fleet region is required");
|
|
507
|
+
const client = options.client ?? (options.token ? createLinodeClient(options.token) : undefined);
|
|
508
|
+
if (!client)
|
|
509
|
+
throw new Error("[deploy/linode] either `token` or `client` must be provided");
|
|
510
|
+
const tag = options.tag ?? "absolutejs-paas-node";
|
|
511
|
+
const normalize = (instance) => {
|
|
512
|
+
const publicIpv42 = instance.ipv4.find((ip) => !isPrivate(ip));
|
|
513
|
+
const privateIpv4 = instance.ipv4.find(isPrivate);
|
|
514
|
+
const agent = infrastructureAgent(options.agent, {
|
|
515
|
+
privateIpv4,
|
|
516
|
+
publicIpv4: publicIpv42
|
|
517
|
+
});
|
|
518
|
+
return {
|
|
519
|
+
id: `linode:${instance.id}`,
|
|
520
|
+
label: instance.label,
|
|
521
|
+
provider: "linode",
|
|
522
|
+
region: instance.region ?? "unknown",
|
|
523
|
+
state: stateFor(instance.status),
|
|
524
|
+
...publicIpv42 ? { publicIpv4: publicIpv42 } : {},
|
|
525
|
+
...privateIpv4 ? { privateIpv4 } : {},
|
|
526
|
+
...agent ? { agent } : {}
|
|
527
|
+
};
|
|
528
|
+
};
|
|
529
|
+
const list = () => listLinodeInstances({ client, tag });
|
|
530
|
+
return {
|
|
531
|
+
capabilities: {
|
|
532
|
+
cloudInit: true,
|
|
533
|
+
idempotentProvisioning: true,
|
|
534
|
+
privateNetworking: true,
|
|
535
|
+
regionalPlacement: true,
|
|
536
|
+
regions: options.regions.map(({ region }) => region)
|
|
537
|
+
},
|
|
538
|
+
getNode: async (id) => {
|
|
539
|
+
const instance = await client.request("GET", `/linode/instances/${parseNodeId(id)}`);
|
|
540
|
+
return normalize(instance);
|
|
541
|
+
},
|
|
542
|
+
listNodes: async () => (await list()).map(normalize),
|
|
543
|
+
name: "linode",
|
|
544
|
+
provisionNode: async (input) => {
|
|
545
|
+
const existing = await findLinodeInstance(client, input.name);
|
|
546
|
+
if (existing)
|
|
547
|
+
return normalize(existing);
|
|
548
|
+
const instances = await list();
|
|
549
|
+
const region = leastPopulatedRegion(options.regions, instances.map((instance2) => instance2.region ?? "unknown"), input.region);
|
|
550
|
+
if (!region)
|
|
551
|
+
throw new Error(`[deploy/linode] region ${input.region ?? "(any)"} is not configured`);
|
|
552
|
+
const instance = await client.request("POST", "/linode/instances", {
|
|
553
|
+
authorized_keys: [...region.sshKeys],
|
|
554
|
+
image: region.image,
|
|
555
|
+
label: input.name,
|
|
556
|
+
private_ip: true,
|
|
557
|
+
region: region.region,
|
|
558
|
+
root_pass: rootPassword(),
|
|
559
|
+
tags: [tag],
|
|
560
|
+
type: region.type,
|
|
561
|
+
...input.userData ?? region.userData ? { stackscript_data: { user_data: input.userData ?? region.userData } } : {}
|
|
562
|
+
});
|
|
563
|
+
return normalize(instance);
|
|
564
|
+
},
|
|
565
|
+
terminateNode: async (id) => destroyLinodeInstance({ client, id: parseNodeId(id) })
|
|
566
|
+
};
|
|
567
|
+
};
|
|
568
|
+
export {
|
|
569
|
+
createLinodeInfrastructureProvider
|
|
570
|
+
};
|
|
571
|
+
|
|
572
|
+
//# debugId=EBD657F73178F71564756E2164756E21
|
|
573
|
+
//# sourceMappingURL=linodeInfrastructure.js.map
|