relight-cli 0.1.0 → 0.3.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 +77 -34
- package/package.json +12 -4
- package/src/cli.js +350 -1
- package/src/commands/apps.js +128 -0
- package/src/commands/auth.js +13 -4
- package/src/commands/config.js +282 -0
- package/src/commands/cost.js +593 -0
- package/src/commands/db.js +775 -0
- package/src/commands/deploy.js +264 -0
- package/src/commands/doctor.js +69 -13
- package/src/commands/domains.js +223 -0
- package/src/commands/logs.js +111 -0
- package/src/commands/open.js +42 -0
- package/src/commands/ps.js +121 -0
- package/src/commands/scale.js +132 -0
- package/src/commands/service.js +227 -0
- package/src/lib/clouds/aws.js +309 -35
- package/src/lib/clouds/cf.js +401 -2
- package/src/lib/clouds/gcp.js +255 -4
- package/src/lib/clouds/neon.js +147 -0
- package/src/lib/clouds/slicervm.js +139 -0
- package/src/lib/config.js +200 -2
- package/src/lib/docker.js +34 -0
- package/src/lib/link.js +31 -5
- package/src/lib/providers/aws/app.js +481 -0
- package/src/lib/providers/aws/db.js +504 -0
- package/src/lib/providers/aws/dns.js +232 -0
- package/src/lib/providers/aws/registry.js +59 -0
- package/src/lib/providers/cf/app.js +596 -0
- package/src/lib/providers/cf/bundle.js +70 -0
- package/src/lib/providers/cf/db.js +181 -0
- package/src/lib/providers/cf/dns.js +148 -0
- package/src/lib/providers/cf/registry.js +17 -0
- package/src/lib/providers/gcp/app.js +429 -0
- package/src/lib/providers/gcp/db.js +372 -0
- package/src/lib/providers/gcp/dns.js +166 -0
- package/src/lib/providers/gcp/registry.js +30 -0
- package/src/lib/providers/neon/db.js +306 -0
- package/src/lib/providers/resolve.js +79 -0
- package/src/lib/providers/slicervm/app.js +396 -0
- package/src/lib/providers/slicervm/db.js +33 -0
- package/src/lib/providers/slicervm/dns.js +58 -0
- package/src/lib/providers/slicervm/registry.js +7 -0
- package/worker-template/package.json +10 -0
- package/worker-template/src/index.js +260 -0
|
@@ -0,0 +1,396 @@
|
|
|
1
|
+
import { execSync } from "child_process";
|
|
2
|
+
import { readFileSync, unlinkSync } from "fs";
|
|
3
|
+
import { tmpdir } from "os";
|
|
4
|
+
import { join } from "path";
|
|
5
|
+
import {
|
|
6
|
+
listNodes,
|
|
7
|
+
createNode,
|
|
8
|
+
deleteNode,
|
|
9
|
+
resumeVM,
|
|
10
|
+
healthCheck,
|
|
11
|
+
execInVM,
|
|
12
|
+
uploadToVM,
|
|
13
|
+
} from "../../clouds/slicervm.js";
|
|
14
|
+
import { status } from "../../output.js";
|
|
15
|
+
|
|
16
|
+
// --- Helpers ---
|
|
17
|
+
|
|
18
|
+
var APP_ROOT = "/app-root";
|
|
19
|
+
var CONFIG_PATH = "/home/ubuntu/.relight.json";
|
|
20
|
+
|
|
21
|
+
function findNodeByApp(nodes, appName) {
|
|
22
|
+
return nodes.find(
|
|
23
|
+
(n) => n.tags && n.tags.includes(appName)
|
|
24
|
+
) || null;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function sleep(ms) {
|
|
28
|
+
return new Promise((r) => setTimeout(r, ms));
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// Parse NDJSON exec response - collect stdout, check exit code
|
|
32
|
+
function parseExecResponse(raw) {
|
|
33
|
+
var text = typeof raw === "string" ? raw : JSON.stringify(raw);
|
|
34
|
+
var stdout = "";
|
|
35
|
+
var stderr = "";
|
|
36
|
+
var exitCode = 0;
|
|
37
|
+
for (var line of text.split("\n")) {
|
|
38
|
+
line = line.trim();
|
|
39
|
+
if (!line) continue;
|
|
40
|
+
try {
|
|
41
|
+
var obj = JSON.parse(line);
|
|
42
|
+
if (obj.stdout) stdout += obj.stdout;
|
|
43
|
+
if (obj.stderr) stderr += obj.stderr;
|
|
44
|
+
if (obj.exit_code !== undefined) exitCode = obj.exit_code;
|
|
45
|
+
} catch {
|
|
46
|
+
// Not JSON - treat as raw output
|
|
47
|
+
stdout += line;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
return { stdout, stderr, exitCode };
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// Run a command in VM and return stdout. Throws on non-zero exit.
|
|
54
|
+
async function vmExec(cfg, hostname, cmd, args, opts) {
|
|
55
|
+
var raw = await execInVM(cfg, hostname, cmd, args, opts);
|
|
56
|
+
var result = parseExecResponse(raw);
|
|
57
|
+
if (result.exitCode !== 0) {
|
|
58
|
+
throw new Error(`Command failed (exit ${result.exitCode}): ${result.stderr || result.stdout}`);
|
|
59
|
+
}
|
|
60
|
+
return result.stdout;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
async function waitForHealth(cfg, hostname, retries = 30) {
|
|
64
|
+
for (var i = 0; i < retries; i++) {
|
|
65
|
+
try {
|
|
66
|
+
await healthCheck(cfg, hostname);
|
|
67
|
+
return;
|
|
68
|
+
} catch {
|
|
69
|
+
await sleep(1000);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
throw new Error(`VM ${hostname} did not become healthy after ${retries}s`);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
async function readAppConfig(cfg, hostname) {
|
|
76
|
+
try {
|
|
77
|
+
var stdout = await vmExec(cfg, hostname, "cat", [CONFIG_PATH]);
|
|
78
|
+
return JSON.parse(stdout);
|
|
79
|
+
} catch {
|
|
80
|
+
return null;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
async function writeAppConfig(cfg, hostname, appConfig) {
|
|
85
|
+
var json = JSON.stringify(appConfig, null, 2);
|
|
86
|
+
await vmExec(cfg, hostname, "sh", [
|
|
87
|
+
"-c",
|
|
88
|
+
`cat > ${CONFIG_PATH} << 'RELIGHT_EOF'\n${json}\nRELIGHT_EOF`,
|
|
89
|
+
]);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function inspectImage(localTag) {
|
|
93
|
+
var raw = execSync(
|
|
94
|
+
`docker inspect --format='{{json .Config}}' ${localTag}`,
|
|
95
|
+
{ encoding: "utf-8", stdio: "pipe" }
|
|
96
|
+
).trim();
|
|
97
|
+
// docker inspect wraps in single quotes on some versions
|
|
98
|
+
if (raw.startsWith("'") && raw.endsWith("'")) {
|
|
99
|
+
raw = raw.slice(1, -1);
|
|
100
|
+
}
|
|
101
|
+
return JSON.parse(raw);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function buildEntrypoint(imageConfig) {
|
|
105
|
+
var entrypoint = imageConfig.Entrypoint || [];
|
|
106
|
+
var cmd = imageConfig.Cmd || [];
|
|
107
|
+
|
|
108
|
+
// Docker resolution: if ENTRYPOINT is set, CMD is appended as args.
|
|
109
|
+
// If only CMD, it's used directly (with shell form handled by Docker already).
|
|
110
|
+
var parts = [...entrypoint, ...cmd];
|
|
111
|
+
if (parts.length === 0) {
|
|
112
|
+
throw new Error(
|
|
113
|
+
"Docker image has no CMD or ENTRYPOINT. Add a CMD to your Dockerfile."
|
|
114
|
+
);
|
|
115
|
+
}
|
|
116
|
+
return parts;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function buildEnvMap(imageConfig, appConfig) {
|
|
120
|
+
var env = {};
|
|
121
|
+
|
|
122
|
+
// Start with image ENV directives
|
|
123
|
+
for (var entry of (imageConfig.Env || [])) {
|
|
124
|
+
var eq = entry.indexOf("=");
|
|
125
|
+
if (eq !== -1) {
|
|
126
|
+
env[entry.substring(0, eq)] = entry.substring(eq + 1);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// Overlay relight app env vars
|
|
131
|
+
if (appConfig.env) {
|
|
132
|
+
for (var key of (appConfig.envKeys || [])) {
|
|
133
|
+
if (appConfig.env[key] !== undefined) env[key] = appConfig.env[key];
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// Always set PORT
|
|
138
|
+
env.PORT = String(appConfig.port || 8080);
|
|
139
|
+
|
|
140
|
+
return env;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// --- App config ---
|
|
144
|
+
|
|
145
|
+
export async function getAppConfig(cfg, appName) {
|
|
146
|
+
var nodes = await listNodes(cfg);
|
|
147
|
+
var node = findNodeByApp(nodes, appName);
|
|
148
|
+
if (!node) return null;
|
|
149
|
+
return readAppConfig(cfg, node.hostname);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
export async function pushAppConfig(cfg, appName, appConfig) {
|
|
153
|
+
var nodes = await listNodes(cfg);
|
|
154
|
+
var node = findNodeByApp(nodes, appName);
|
|
155
|
+
if (!node) throw new Error(`No VM found for app ${appName}`);
|
|
156
|
+
await writeAppConfig(cfg, node.hostname, appConfig);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// --- Deploy ---
|
|
160
|
+
|
|
161
|
+
export async function deploy(cfg, appName, imageTag, opts) {
|
|
162
|
+
var appConfig = opts.appConfig;
|
|
163
|
+
var isFirstDeploy = opts.isFirstDeploy;
|
|
164
|
+
|
|
165
|
+
// 1. Inspect Docker image for CMD, ENTRYPOINT, ENV, WORKDIR
|
|
166
|
+
status("Inspecting image...");
|
|
167
|
+
var imageConfig = inspectImage(imageTag);
|
|
168
|
+
var entrypoint = buildEntrypoint(imageConfig);
|
|
169
|
+
var workdir = imageConfig.WorkingDir || "/";
|
|
170
|
+
|
|
171
|
+
// 2. Find or create VM node
|
|
172
|
+
status("Finding VM...");
|
|
173
|
+
var nodes = await listNodes(cfg);
|
|
174
|
+
var node = findNodeByApp(nodes, appName);
|
|
175
|
+
|
|
176
|
+
if (!node) {
|
|
177
|
+
status("Creating VM...");
|
|
178
|
+
node = await createNode(cfg, cfg.hostGroup, {
|
|
179
|
+
tags: [appName],
|
|
180
|
+
vcpu: appConfig.vcpu,
|
|
181
|
+
memory: appConfig.memory,
|
|
182
|
+
});
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
var hostname = node.hostname;
|
|
186
|
+
|
|
187
|
+
// 3. Resume if paused
|
|
188
|
+
if (node.status === "Paused" || node.status === "paused") {
|
|
189
|
+
status("Resuming VM...");
|
|
190
|
+
await resumeVM(cfg, hostname);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// 4. Wait for health
|
|
194
|
+
status("Waiting for VM...");
|
|
195
|
+
await waitForHealth(cfg, hostname);
|
|
196
|
+
|
|
197
|
+
// 5. Stop old app if redeploying
|
|
198
|
+
if (!isFirstDeploy) {
|
|
199
|
+
status("Stopping old app...");
|
|
200
|
+
try {
|
|
201
|
+
await vmExec(cfg, hostname, "sh", [
|
|
202
|
+
"-c",
|
|
203
|
+
`kill $(cat /run/relight-app.pid 2>/dev/null) 2>/dev/null; rm -f /run/relight-app.pid; sleep 1`,
|
|
204
|
+
], { uid: 0 });
|
|
205
|
+
} catch {}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// 6. Export Docker image filesystem as tar
|
|
209
|
+
status("Extracting image...");
|
|
210
|
+
var containerId = execSync(`docker create ${imageTag}`, {
|
|
211
|
+
encoding: "utf-8",
|
|
212
|
+
stdio: "pipe",
|
|
213
|
+
}).trim();
|
|
214
|
+
|
|
215
|
+
var tarPath = join(tmpdir(), `relight-${appName}-${Date.now()}.tar`);
|
|
216
|
+
try {
|
|
217
|
+
execSync(`docker export ${containerId} -o ${tarPath}`, { stdio: "pipe" });
|
|
218
|
+
} finally {
|
|
219
|
+
execSync(`docker rm ${containerId}`, { stdio: "pipe" });
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// 7. Upload tar to VM at /app-root (the chroot target)
|
|
223
|
+
status("Uploading to VM...");
|
|
224
|
+
var tarBuffer = readFileSync(tarPath);
|
|
225
|
+
await uploadToVM(cfg, hostname, APP_ROOT, tarBuffer, {
|
|
226
|
+
mode: "tar",
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
try { unlinkSync(tarPath); } catch {}
|
|
230
|
+
|
|
231
|
+
// 8. Set up chroot: mounts + busybox symlinks (requires root)
|
|
232
|
+
status("Preparing chroot...");
|
|
233
|
+
await vmExec(cfg, hostname, "sh", [
|
|
234
|
+
"-c",
|
|
235
|
+
[
|
|
236
|
+
`mkdir -p ${APP_ROOT}/proc ${APP_ROOT}/sys ${APP_ROOT}/dev ${APP_ROOT}/dev/pts ${APP_ROOT}/tmp`,
|
|
237
|
+
`mountpoint -q ${APP_ROOT}/proc || mount -t proc proc ${APP_ROOT}/proc`,
|
|
238
|
+
`mountpoint -q ${APP_ROOT}/sys || mount -t sysfs sysfs ${APP_ROOT}/sys`,
|
|
239
|
+
`mountpoint -q ${APP_ROOT}/dev || mount --bind /dev ${APP_ROOT}/dev`,
|
|
240
|
+
`mountpoint -q ${APP_ROOT}/dev/pts || mount --bind /dev/pts ${APP_ROOT}/dev/pts`,
|
|
241
|
+
// Copy resolv.conf so DNS works inside chroot
|
|
242
|
+
`cp /etc/resolv.conf ${APP_ROOT}/etc/resolv.conf 2>/dev/null || true`,
|
|
243
|
+
// docker export flattens layers and loses symlinks. Fix up:
|
|
244
|
+
// 1. Alpine: busybox symlinks (sh, ls, etc.)
|
|
245
|
+
`if [ -f ${APP_ROOT}/bin/busybox ] && [ ! -e ${APP_ROOT}/bin/sh ]; then chroot ${APP_ROOT} /bin/busybox --install -s /bin; fi`,
|
|
246
|
+
// 2. Recreate missing .so symlinks (e.g. libstdc++.so.6 -> libstdc++.so.6.0.34)
|
|
247
|
+
`find ${APP_ROOT}/usr/lib ${APP_ROOT}/lib -name '*.so.*.*' -type f 2>/dev/null | while read f; do base=$(basename "$f"); dir=$(dirname "$f"); major=$(echo "$base" | sed 's/\\(.*\\.so\\.[0-9]*\\).*/\\1/'); [ "$major" != "$base" ] && [ ! -e "$dir/$major" ] && ln -sf "$base" "$dir/$major"; done`,
|
|
248
|
+
].join(" && "),
|
|
249
|
+
], { uid: 0 });
|
|
250
|
+
|
|
251
|
+
// 9. Write relight config outside the chroot (on the VM root)
|
|
252
|
+
status("Writing config...");
|
|
253
|
+
await writeAppConfig(cfg, hostname, appConfig);
|
|
254
|
+
|
|
255
|
+
// 10. Build env and start command
|
|
256
|
+
var env = buildEnvMap(imageConfig, appConfig);
|
|
257
|
+
|
|
258
|
+
// Build env export commands for inside the chroot
|
|
259
|
+
var envExport = Object.entries(env)
|
|
260
|
+
.map(([k, v]) => `export ${k}=${shellQuote(v)}`)
|
|
261
|
+
.join("; ");
|
|
262
|
+
|
|
263
|
+
var entrypointStr = entrypoint.map(shellQuote).join(" ");
|
|
264
|
+
var innerCmd = `cd ${shellQuote(workdir)} && exec ${entrypointStr}`;
|
|
265
|
+
|
|
266
|
+
// Full command: set env, chroot, run entrypoint, log output, track PID
|
|
267
|
+
// Needs root for chroot
|
|
268
|
+
var chrootCmd = [
|
|
269
|
+
`${envExport}`,
|
|
270
|
+
`chroot ${APP_ROOT} /bin/sh -c '${innerCmd.replace(/'/g, "'\\''")}'`,
|
|
271
|
+
].join("; ");
|
|
272
|
+
|
|
273
|
+
status("Starting app...");
|
|
274
|
+
await vmExec(cfg, hostname, "sh", [
|
|
275
|
+
"-c",
|
|
276
|
+
`nohup sh -c '${chrootCmd.replace(/'/g, "'\\''")}' > /var/log/relight-app.log 2>&1 & echo $! > /run/relight-app.pid`,
|
|
277
|
+
], { uid: 0 });
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
function shellQuote(s) {
|
|
281
|
+
return "'" + String(s).replace(/'/g, "'\\''") + "'";
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
// --- List apps ---
|
|
285
|
+
|
|
286
|
+
export async function listApps(cfg) {
|
|
287
|
+
var nodes = await listNodes(cfg);
|
|
288
|
+
var apps = [];
|
|
289
|
+
for (var node of nodes) {
|
|
290
|
+
if (!node.tags || node.tags.length === 0) continue;
|
|
291
|
+
var appName = node.tags[0];
|
|
292
|
+
var config = null;
|
|
293
|
+
try {
|
|
294
|
+
config = await readAppConfig(cfg, node.hostname);
|
|
295
|
+
} catch {}
|
|
296
|
+
apps.push({
|
|
297
|
+
name: appName,
|
|
298
|
+
modified: config?.deployedAt || null,
|
|
299
|
+
});
|
|
300
|
+
}
|
|
301
|
+
return apps;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
// --- Get app info ---
|
|
305
|
+
|
|
306
|
+
export async function getAppInfo(cfg, appName) {
|
|
307
|
+
var appConfig = await getAppConfig(cfg, appName);
|
|
308
|
+
if (!appConfig) return null;
|
|
309
|
+
var url = `https://${appName}.${cfg.baseDomain}`;
|
|
310
|
+
return { appConfig, url };
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
// --- Destroy ---
|
|
314
|
+
|
|
315
|
+
export async function destroyApp(cfg, appName) {
|
|
316
|
+
var nodes = await listNodes(cfg);
|
|
317
|
+
var node = findNodeByApp(nodes, appName);
|
|
318
|
+
if (!node) throw new Error(`No VM found for app ${appName}`);
|
|
319
|
+
await deleteNode(cfg, cfg.hostGroup, node.hostname);
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
// --- Scale ---
|
|
323
|
+
|
|
324
|
+
export async function scale(cfg, appName, opts) {
|
|
325
|
+
var appConfig = opts.appConfig;
|
|
326
|
+
await pushAppConfig(cfg, appName, appConfig);
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
// --- Container status ---
|
|
330
|
+
|
|
331
|
+
export async function getContainerStatus(cfg, appName) {
|
|
332
|
+
var nodes = await listNodes(cfg);
|
|
333
|
+
var node = findNodeByApp(nodes, appName);
|
|
334
|
+
if (!node) return [];
|
|
335
|
+
return [
|
|
336
|
+
{
|
|
337
|
+
dimensions: {
|
|
338
|
+
hostname: node.hostname,
|
|
339
|
+
status: node.status || "Unknown",
|
|
340
|
+
region: "self-hosted",
|
|
341
|
+
},
|
|
342
|
+
avg: {},
|
|
343
|
+
},
|
|
344
|
+
];
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
// --- App URL ---
|
|
348
|
+
|
|
349
|
+
export async function getAppUrl(cfg, appName) {
|
|
350
|
+
var appConfig = await getAppConfig(cfg, appName);
|
|
351
|
+
if (appConfig?.domains?.length > 0) {
|
|
352
|
+
return `https://${appConfig.domains[0]}`;
|
|
353
|
+
}
|
|
354
|
+
return `https://${appName}.${cfg.baseDomain}`;
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
// --- Costs ---
|
|
358
|
+
|
|
359
|
+
export async function getCosts(cfg, appNames, dateRange) {
|
|
360
|
+
var names = appNames || [];
|
|
361
|
+
if (!appNames) {
|
|
362
|
+
var apps = await listApps(cfg);
|
|
363
|
+
names = apps.map((a) => a.name);
|
|
364
|
+
}
|
|
365
|
+
return names.map((name) => ({
|
|
366
|
+
name,
|
|
367
|
+
usage: {},
|
|
368
|
+
}));
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
// --- Log streaming ---
|
|
372
|
+
|
|
373
|
+
export async function streamLogs(cfg, appName) {
|
|
374
|
+
var nodes = await listNodes(cfg);
|
|
375
|
+
var node = findNodeByApp(nodes, appName);
|
|
376
|
+
if (!node) throw new Error(`No VM found for app ${appName}`);
|
|
377
|
+
|
|
378
|
+
var res = await execInVM(cfg, node.hostname, "tail", ["-f", "/var/log/relight-app.log"], {
|
|
379
|
+
stream: true,
|
|
380
|
+
});
|
|
381
|
+
|
|
382
|
+
return {
|
|
383
|
+
url: null,
|
|
384
|
+
id: node.hostname,
|
|
385
|
+
reader: res.body,
|
|
386
|
+
cleanup: async () => {},
|
|
387
|
+
};
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
// --- Regions ---
|
|
391
|
+
|
|
392
|
+
export function getRegions() {
|
|
393
|
+
return [
|
|
394
|
+
{ code: "self-hosted", name: "Self-hosted", location: "Your infrastructure" },
|
|
395
|
+
];
|
|
396
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
var MSG = "SlicerVM does not include managed databases. Use an external database service.";
|
|
2
|
+
|
|
3
|
+
export async function createDatabase() {
|
|
4
|
+
throw new Error(MSG);
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export async function destroyDatabase() {
|
|
8
|
+
throw new Error(MSG);
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export async function getDatabaseInfo() {
|
|
12
|
+
throw new Error(MSG);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export async function queryDatabase() {
|
|
16
|
+
throw new Error(MSG);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export async function importDatabase() {
|
|
20
|
+
throw new Error(MSG);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export async function exportDatabase() {
|
|
24
|
+
throw new Error(MSG);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export async function rotateToken() {
|
|
28
|
+
throw new Error(MSG);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export async function resetDatabase() {
|
|
32
|
+
throw new Error(MSG);
|
|
33
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { listNodes } from "../../clouds/slicervm.js";
|
|
2
|
+
import { getAppConfig, pushAppConfig } from "./app.js";
|
|
3
|
+
|
|
4
|
+
export async function listDomains(cfg, appName) {
|
|
5
|
+
var appConfig = await getAppConfig(cfg, appName);
|
|
6
|
+
var defaultDomain = `${appName}.${cfg.baseDomain}`;
|
|
7
|
+
|
|
8
|
+
return {
|
|
9
|
+
default: defaultDomain,
|
|
10
|
+
custom: appConfig?.domains || [],
|
|
11
|
+
};
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export async function addDomain(cfg, appName, domain) {
|
|
15
|
+
var appConfig = await getAppConfig(cfg, appName);
|
|
16
|
+
if (!appConfig) {
|
|
17
|
+
throw new Error(`App ${appName} not found.`);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
if (!appConfig.domains) appConfig.domains = [];
|
|
21
|
+
if (appConfig.domains.includes(domain)) {
|
|
22
|
+
throw new Error(`Domain ${domain} is already attached to ${appName}.`);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// Add domain as a tag on the VM node so Caddy can route to it
|
|
26
|
+
var nodes = await listNodes(cfg);
|
|
27
|
+
var node = nodes.find((n) => n.tags && n.tags.includes(appName));
|
|
28
|
+
if (node && !node.tags.includes(domain)) {
|
|
29
|
+
node.tags.push(domain);
|
|
30
|
+
// Tags are updated via the app config - the Caddy module reads tags from the node
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
appConfig.domains.push(domain);
|
|
34
|
+
await pushAppConfig(cfg, appName, appConfig);
|
|
35
|
+
|
|
36
|
+
process.stderr.write(
|
|
37
|
+
`\n Point your DNS A record for ${domain} to your Slicer host IP.\n`
|
|
38
|
+
);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export async function removeDomain(cfg, appName, domain) {
|
|
42
|
+
var appConfig = await getAppConfig(cfg, appName);
|
|
43
|
+
if (!appConfig) {
|
|
44
|
+
throw new Error(`App ${appName} not found.`);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
appConfig.domains = (appConfig.domains || []).filter((d) => d !== domain);
|
|
48
|
+
await pushAppConfig(cfg, appName, appConfig);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// SlicerVM doesn't use zones - custom domains use VM tags + manual DNS
|
|
52
|
+
export async function getZones() {
|
|
53
|
+
return [];
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export function findZoneForHostname() {
|
|
57
|
+
return null;
|
|
58
|
+
}
|