Compare commits
2 commits
e349bc7cad
...
663c785458
Author | SHA1 | Date | |
---|---|---|---|
663c785458 | |||
bc33fd38cb |
6 changed files with 129 additions and 21 deletions
|
@ -4,6 +4,8 @@
|
|||
|
||||
una API para correr cosas aisladas en una VM [firecracker] que inicia rápido a partir de una imágen de contenedor OCI. inspirado en [Fly Machines] y otras cosas.
|
||||
|
||||
[notas y referencias sobre firecracker](https://nulo.ar/Firecracker.html) en mi sitio
|
||||
|
||||
[firecracker]: <https://github.com/firecracker-microvm/firecracker>
|
||||
[Fly Machines]: <https://fly.io/docs/reference/machines/>
|
||||
|
||||
|
|
3
server/.gitignore
vendored
3
server/.gitignore
vendored
|
@ -3,3 +3,6 @@ container-baby.js
|
|||
|
||||
# cache, will move later
|
||||
cache/
|
||||
|
||||
# jailer
|
||||
jailer/
|
115
server/index.js
115
server/index.js
|
@ -1,11 +1,13 @@
|
|||
import { delay } from "nanodelay";
|
||||
import { nanoid } from "nanoid";
|
||||
import { customAlphabet as nanoidCustomAlphabet, nanoid } from "nanoid";
|
||||
import which from "which";
|
||||
import { spawn, execFile as _execFile } from "node:child_process";
|
||||
import { createServer, request as _request } from "node:http";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
import { basename, join } from "node:path";
|
||||
import { downloadImage, parseImageRef } from "./container-baby.js";
|
||||
import { promisify } from "node:util";
|
||||
import { access, chmod, chown, link, mkdir } from "node:fs/promises";
|
||||
const execFile = promisify(_execFile);
|
||||
|
||||
const server = createServer(listener);
|
||||
|
@ -55,52 +57,115 @@ async function runVm(image) {
|
|||
* @prop {boolean} is_read_only
|
||||
*/
|
||||
|
||||
/**
|
||||
* gets absolute path to firecracker executable
|
||||
* @returns {Promise<string>}
|
||||
*/
|
||||
async function getFirecrackerExe() {
|
||||
const firecrackerExe = await which("firecracker", { nothrow: true });
|
||||
console.debug(
|
||||
`[fireactions] Using firecracker executable '${firecrackerExe}'`
|
||||
);
|
||||
return firecrackerExe;
|
||||
}
|
||||
|
||||
const jailerDir = "./jailer";
|
||||
const [jailerUid, jailerGid] = [65534, 65534];
|
||||
|
||||
// we need to use a custom alphabet because jailer is picky
|
||||
const jailerNanoid = nanoidCustomAlphabet(
|
||||
"0123456789abcdefghijklmnopqrstuvwxyz-",
|
||||
64
|
||||
);
|
||||
|
||||
class FirecrackerInstance {
|
||||
proc;
|
||||
vmid;
|
||||
socketPath;
|
||||
constructor() {
|
||||
const vmid = nanoid();
|
||||
this.socketPath = join(tmpdir(), `firecracker-${vmid}.sock`);
|
||||
proc;
|
||||
/**
|
||||
* @param {{ firecrackerExe: string }} param0
|
||||
* @private
|
||||
*/
|
||||
constructor({ firecrackerExe }) {
|
||||
this.vmid = jailerNanoid();
|
||||
this.socketPath = join(this.chrootPath, `firecracker.sock`);
|
||||
console.debug(this.socketPath);
|
||||
// TODO: jailer
|
||||
this.proc = spawn(
|
||||
"firecracker",
|
||||
"jailer",
|
||||
[
|
||||
...["--api-sock", this.socketPath],
|
||||
...["--level", "Debug"],
|
||||
...["--log-path", "/dev/stderr"],
|
||||
"--show-level",
|
||||
"--show-log-origin",
|
||||
...["--id", this.vmid],
|
||||
...["--exec-file", firecrackerExe],
|
||||
...["--uid", "" + jailerUid],
|
||||
...["--gid", "" + jailerGid],
|
||||
...["--chroot-base-dir", jailerDir],
|
||||
"--",
|
||||
...["--api-sock", "/firecracker.sock"],
|
||||
// ...["--level", "Debug"],
|
||||
// ...["--log-path", "/stderr.log"],
|
||||
// "--show-level",
|
||||
// "--show-log-origin",
|
||||
],
|
||||
{
|
||||
stdio: "inherit",
|
||||
}
|
||||
);
|
||||
|
||||
console.debug(
|
||||
`[fireactions] Started firecracker with jailer at ${this.chrootPath}`
|
||||
);
|
||||
}
|
||||
|
||||
get chrootPath() {
|
||||
return join(jailerDir, "firecracker", this.vmid, "root");
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} path path to file to hardlink to root of chroot
|
||||
* @param {number} mod mod inside the chroot
|
||||
*/
|
||||
async hardlinkToChroot(path, mod = 0o600) {
|
||||
const chrootPath = join(this.chrootPath, basename(path));
|
||||
let accesed = false;
|
||||
try {
|
||||
await access(chrootPath);
|
||||
accesed = true;
|
||||
} catch {}
|
||||
if (accesed)
|
||||
throw new Error("file with same name already exists inside chroot");
|
||||
await link(path, chrootPath);
|
||||
await chown(chrootPath, jailerUid, jailerGid);
|
||||
await chmod(chrootPath, mod);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param [opts] {{ drives?: Drive[] }}
|
||||
*/
|
||||
static async run(opts) {
|
||||
const self = new FirecrackerInstance();
|
||||
await mkdir(jailerDir, { recursive: true });
|
||||
const self = new FirecrackerInstance({
|
||||
firecrackerExe: await getFirecrackerExe(),
|
||||
});
|
||||
await mkdir(self.chrootPath, { recursive: true });
|
||||
// TODO: retry until success
|
||||
await delay(15);
|
||||
|
||||
const { ifname, hostAddr, guestAddr } = await createNetworkInterface();
|
||||
|
||||
await self.hardlinkToChroot("../vmlinux.bin", 0o400);
|
||||
await self.request("PUT", "/boot-source", {
|
||||
kernel_image_path: "../vmlinux.bin",
|
||||
kernel_image_path: "/vmlinux.bin",
|
||||
boot_args: `console=ttyS0 reboot=k panic=1 pci=off fireactions_ip=${guestAddr}:${hostAddr}`,
|
||||
});
|
||||
await self.request("PUT", "/drives/rootfs", {
|
||||
// TODO: readonly
|
||||
self.attachDrive({
|
||||
drive_id: "rootfs",
|
||||
path_on_host: "../rootfs.ext4",
|
||||
path_on_host: "/rootfs.ext4",
|
||||
is_root_device: true,
|
||||
// TODO: readonly
|
||||
is_read_only: false,
|
||||
});
|
||||
if (opts?.drives) {
|
||||
for (const drive of opts.drives) {
|
||||
await self.request("PUT", "/drives/" + drive.drive_id, drive);
|
||||
await self.attachDrive(drive);
|
||||
}
|
||||
}
|
||||
await self.request("PUT", "/network-interfaces/eth0", {
|
||||
|
@ -119,6 +184,18 @@ class FirecrackerInstance {
|
|||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Drive} drive
|
||||
*/
|
||||
async attachDrive(drive) {
|
||||
const perms = drive.is_read_only ? 0o400 : 0o600;
|
||||
await this.hardlinkToChroot(drive.path_on_host, perms);
|
||||
await this.request("PUT", "/drives/" + drive.drive_id, {
|
||||
...drive,
|
||||
path_on_host: "/" + basename(drive.path_on_host),
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} method
|
||||
* @param {string} path
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
"description": "",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
"start": "esbuild --bundle index.js --platform=node --sourcemap > build.cjs && sudo node --enable-source-maps build.cjs"
|
||||
"start": "esbuild --bundle index.js --platform=node --sourcemap > build.cjs && sudo env PATH=\"$PATH:$HOME/.local/bin\" node --enable-source-maps build.cjs"
|
||||
},
|
||||
"keywords": [],
|
||||
"author": "",
|
||||
|
@ -15,12 +15,14 @@
|
|||
"@types/gunzip-maybe": "^1.4.0",
|
||||
"@types/node": "^20.2.5",
|
||||
"@types/tar-stream": "^2.2.2",
|
||||
"@types/which": "^3.0.0",
|
||||
"undici": "^5.22.1"
|
||||
},
|
||||
"dependencies": {
|
||||
"gunzip-maybe": "^1.4.2",
|
||||
"nanodelay": "^2.0.2",
|
||||
"nanoid": "^4.0.2",
|
||||
"tar-stream": "^3.0.0"
|
||||
"tar-stream": "^3.0.0",
|
||||
"which": "^3.0.1"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -17,6 +17,9 @@ dependencies:
|
|||
tar-stream:
|
||||
specifier: ^3.0.0
|
||||
version: 3.0.0
|
||||
which:
|
||||
specifier: ^3.0.1
|
||||
version: 3.0.1
|
||||
|
||||
devDependencies:
|
||||
'@tsconfig/node16':
|
||||
|
@ -31,6 +34,9 @@ devDependencies:
|
|||
'@types/tar-stream':
|
||||
specifier: ^2.2.2
|
||||
version: 2.2.2
|
||||
'@types/which':
|
||||
specifier: ^3.0.0
|
||||
version: 3.0.0
|
||||
undici:
|
||||
specifier: ^5.22.1
|
||||
version: 5.22.1
|
||||
|
@ -57,6 +63,10 @@ packages:
|
|||
'@types/node': 20.2.5
|
||||
dev: true
|
||||
|
||||
/@types/which@3.0.0:
|
||||
resolution: {integrity: sha512-ASCxdbsrwNfSMXALlC3Decif9rwDMu+80KGp5zI2RLRotfMsTv7fHL8W8VDp24wymzDyIFudhUeSCugrgRFfHQ==}
|
||||
dev: true
|
||||
|
||||
/abort-controller@3.0.0:
|
||||
resolution: {integrity: sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==}
|
||||
engines: {node: '>=6.5'}
|
||||
|
@ -170,6 +180,10 @@ packages:
|
|||
resolution: {integrity: sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==}
|
||||
dev: false
|
||||
|
||||
/isexe@2.0.0:
|
||||
resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==}
|
||||
dev: false
|
||||
|
||||
/nanodelay@2.0.2:
|
||||
resolution: {integrity: sha512-6AS5aCSXsjoxq2Jr9CdaAeT60yoYDOTp6po9ziqeOeY6vf6uTEHYSqWql6EFILrM3fEfXgkZ4KqE9L0rTm/wlA==}
|
||||
engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0}
|
||||
|
@ -301,6 +315,14 @@ packages:
|
|||
resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==}
|
||||
dev: false
|
||||
|
||||
/which@3.0.1:
|
||||
resolution: {integrity: sha512-XA1b62dzQzLfaEOSQFTCOd5KFf/1VSzZo7/7TUjnya6u0vGGKzU96UQBZTAThCb2j4/xjBAyii1OhRLJEivHvg==}
|
||||
engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0}
|
||||
hasBin: true
|
||||
dependencies:
|
||||
isexe: 2.0.0
|
||||
dev: false
|
||||
|
||||
/wrappy@1.0.2:
|
||||
resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==}
|
||||
dev: false
|
||||
|
|
|
@ -2,6 +2,8 @@
|
|||
// TODO: sandboxear mkquashfs
|
||||
// TODO: fijarse como hace firecracker-containerd
|
||||
// TODO: fijarse como hace [ignite](https://github.com/weaveworks/ignite)
|
||||
// TODO: fijarse como hace [thi](https://github.com/thi-startup/init)
|
||||
// TODO: fijarse como hace [firebuild](https://combust-labs.github.io/firebuild-docs/)
|
||||
|
||||
import gunzip from "gunzip-maybe";
|
||||
import { spawn } from "node:child_process";
|
||||
|
|
Loading…
Reference in a new issue