135 lines
4.4 KiB
TypeScript
135 lines
4.4 KiB
TypeScript
// This assumes an installation of Docker exists on the given host
|
|
// and that host is configured to accept our SSH key
|
|
import config from "../config.ts";
|
|
import { VMManager, type VM } from "./base.ts";
|
|
import { imageName } from "./utils.ts";
|
|
import fs from "node:fs";
|
|
import { homedir } from "node:os";
|
|
import { NodeSSH } from "node-ssh";
|
|
|
|
export class Docker extends VMManager {
|
|
// TODO support multiple Docker providers in the pool config with same region
|
|
size = "";
|
|
largeSize = "";
|
|
minRetries = 0;
|
|
reuseVMs = false;
|
|
id = "Docker";
|
|
ssh: NodeSSH | undefined = undefined;
|
|
imageId = imageName;
|
|
|
|
getSSH = async () => {
|
|
if (this.ssh && this.ssh.isConnected()) {
|
|
return this.ssh;
|
|
}
|
|
const sshConfig = {
|
|
username: config.DOCKER_VM_HOST_SSH_USER,
|
|
host: this.hostname,
|
|
// The private key the Docker host is configured to accept
|
|
privateKey: config.DOCKER_VM_HOST_SSH_KEY_BASE64
|
|
? Buffer.from(config.DOCKER_VM_HOST_SSH_KEY_BASE64, "base64").toString()
|
|
: fs.readFileSync(homedir() + "/.ssh/id_rsa").toString(),
|
|
};
|
|
this.ssh = new NodeSSH();
|
|
await this.ssh.connect(sshConfig);
|
|
return this.ssh;
|
|
};
|
|
|
|
startVM = async (name: string) => {
|
|
const tag = this.getTag();
|
|
const conn = await this.getSSH();
|
|
// If in development, have neko share the same SSL cert as the other services
|
|
// If in production, they are probably on different hosts and neko is behind a reverse proxy for SSL termination
|
|
const sslEnv =
|
|
config.NODE_ENV === "development" &&
|
|
config.SSL_KEY_FILE &&
|
|
config.SSL_CRT_FILE
|
|
? `-e NEKO_KEY="${config.SSL_KEY_FILE}" -e NEKO_CERT="${config.SSL_CRT_FILE}"`
|
|
: "";
|
|
const { stdout, stderr } = await conn.execCommand(
|
|
`
|
|
#!/bin/bash
|
|
set -e
|
|
PORT=$(comm -23 <(seq 5000 5063 | sort) <(ss -Htan | awk '{print $4}' | cut -d':' -f2 | sort -u) | sort -n | head -n 1)
|
|
INDEX=$(($PORT - 5000))
|
|
UDP_START=$((59000+$INDEX*100))
|
|
UDP_END=$((59099+$INDEX*100))
|
|
docker run -d --rm --name=${name} --memory="2g" --cpus="2" -p $PORT:$PORT -p $UDP_START-$UDP_END:$UDP_START-$UDP_END/udp -v /etc/letsencrypt:/etc/letsencrypt -l ${tag} -l index=$INDEX --log-opt max-size=1g --shm-size=1g --cap-add="SYS_ADMIN" ${sslEnv} -e DISPLAY=":99.0" -e NEKO_PASSWORD=${name} -e NEKO_PASSWORD_ADMIN=${name} -e NEKO_ADMIN_KEY=${config.VBROWSER_ADMIN_KEY} -e NEKO_BIND=":$PORT" -e NEKO_EPR=":$UDP_START-$UDP_END" -e NEKO_H264="1" ${imageName}
|
|
`,
|
|
);
|
|
console.log(stdout, stderr);
|
|
return stdout.trim();
|
|
};
|
|
|
|
terminateVM = async (id: string) => {
|
|
const conn = await this.getSSH();
|
|
const { stdout, stderr } = await conn.execCommand(`docker rm -fv ${id}`);
|
|
console.log(stdout, stderr);
|
|
return;
|
|
};
|
|
|
|
rebootVM = async (id: string) => {
|
|
// Docker containers aren't set to reuse, so do nothing (reset will terminate)
|
|
};
|
|
|
|
reimageVM = async (id: string) => {
|
|
const conn = await this.getSSH();
|
|
const { stdout, stderr } = await conn.execCommand(
|
|
`docker pull ${this.imageId}`,
|
|
);
|
|
console.log(stdout, stderr);
|
|
// The container is out of date. Delete it
|
|
this.terminateVMWrapper(id);
|
|
return;
|
|
};
|
|
|
|
getVM = async (id: string) => {
|
|
const conn = await this.getSSH();
|
|
const { stdout } = await conn.execCommand(`docker inspect ${id}`);
|
|
let data = null;
|
|
try {
|
|
data = JSON.parse(stdout)[0];
|
|
if (!data) {
|
|
throw new Error("no container with this ID found");
|
|
}
|
|
} catch {
|
|
console.warn(stdout);
|
|
throw new Error("failed to parse json");
|
|
}
|
|
let server = this.mapServerObject(data);
|
|
return server;
|
|
};
|
|
|
|
listVMs = async (filter: string) => {
|
|
const conn = await this.getSSH();
|
|
const listCmd = `docker inspect $(docker ps --filter label=${filter} --quiet --no-trunc)`;
|
|
const { stdout } = await conn.execCommand(listCmd);
|
|
if (!stdout) {
|
|
return [];
|
|
}
|
|
let data = [];
|
|
try {
|
|
data = JSON.parse(stdout);
|
|
} catch (e) {
|
|
console.warn(stdout);
|
|
throw new Error("failed to parse json");
|
|
}
|
|
return data.map(this.mapServerObject);
|
|
};
|
|
|
|
powerOn = async (id: string) => {};
|
|
|
|
attachToNetwork = async (id: string) => {};
|
|
|
|
updateSnapshot = async () => {
|
|
return "";
|
|
};
|
|
|
|
mapServerObject = (server: any): VM => ({
|
|
id: server.Id,
|
|
host: `${this.hostname}:${5000 + Number(server.Config?.Labels?.index)}`,
|
|
provider: this.id,
|
|
large: this.isLarge,
|
|
region: this.region,
|
|
});
|
|
}
|