fireactions/js/index.js

144 lines
3.6 KiB
JavaScript

import { delay } from "nanodelay";
import { nanoid } from "nanoid";
import { ChildProcess, execFile, spawn } from "node:child_process";
import { writeFile } from "node:fs/promises";
import { createServer, request as _request, IncomingMessage } from "node:http";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { promisify } from "node:util";
const server = createServer(listener);
/**
* @param {import("node:http").IncomingMessage} req
* @param {import("node:http").ServerResponse<import("node:http").IncomingMessage>} res
*/
async function listener(req, res) {
const url = getUrl(req);
if (url.pathname == "/run") {
if (req.method == "POST") {
await runVm();
} else res.writeHead(405).end("wrong method");
} else res.writeHead(404).end("not found");
}
async function runVm() {
try {
await FirecrackerInstance.run();
} catch (err) {
console.error(err);
}
}
class FirecrackerInstance {
proc;
socketPath;
constructor() {
const vmid = nanoid();
this.socketPath = join(tmpdir(), `firecracker-${vmid}.sock`);
console.debug(this.socketPath);
// TODO: jailer
this.proc = spawn(
"firecracker",
[
...["--api-sock", this.socketPath],
...["--level", "Debug"],
...["--log-path", "/dev/stderr"],
"--show-level",
"--show-log-origin",
],
{
stdio: "inherit",
}
);
}
static async run() {
const self = new FirecrackerInstance();
await self.request("PUT", "/boot-source", {
kernel_image_path: "../vmlinux.bin",
boot_args: "console=ttyS0 reboot=k panic=1 pci=off",
});
await self.request("PUT", "/drives/rootfs", {
drive_id: "rootfs",
path_on_host: "../rootfs.ext4",
is_root_device: true,
is_read_only: false,
});
// API requests are handled asynchronously, it is important the configuration is
// set, before `InstanceStart`.
await delay(15);
await self.request("PUT", "/actions", {
action_type: "InstanceStart",
});
}
/**
* @param {string} method
* @param {string} path
* @param {object} body
*/
async request(method, path, body) {
const jsonBody = JSON.stringify(body);
const { response, body: respBody } = await request(
{
method,
path,
socketPath: this.socketPath,
headers: {
"Content-Type": "application/json",
"Content-Length": jsonBody.length,
},
},
jsonBody
);
// @ts-ignore
if (response.statusCode > 299)
throw new Error(
`Status code: ${response.statusCode} / Body: ${respBody}`,
{
cause: `${method} ${path} ${JSON.stringify(body, null, 4)}`,
}
);
}
}
/**
* @param {import("http").IncomingMessage} req
*/
function getUrl(req) {
// prettier-ignore
const urlString = /** @type {string} */(req.url);
const url = new URL(urlString, `http://${req.headers.host}`);
return url;
}
/**
* @param {string | import("url").URL | import("http").RequestOptions} opts
* @param {string} [body]
* @returns {Promise<{ response: import("node:http").IncomingMessage, body: string }>}
*/
function request(opts, body) {
return new Promise((resolve, reject) => {
const req = _request(opts, (res) => {
let respBody = "";
res.on("data", (data) => {
respBody += data;
});
res.on("end", () => {
resolve({ response: res, body: respBody });
});
});
req.on("error", reject);
if (body)
req.write(body, (error) => {
if (error) reject(error);
});
req.end();
});
}
server.listen("8080");