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.
|
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>
|
[firecracker]: <https://github.com/firecracker-microvm/firecracker>
|
||||||
[Fly Machines]: <https://fly.io/docs/reference/machines/>
|
[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, will move later
|
||||||
cache/
|
cache/
|
||||||
|
|
||||||
|
# jailer
|
||||||
|
jailer/
|
115
server/index.js
115
server/index.js
|
@ -1,11 +1,13 @@
|
||||||
import { delay } from "nanodelay";
|
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 { spawn, execFile as _execFile } from "node:child_process";
|
||||||
import { createServer, request as _request } from "node:http";
|
import { createServer, request as _request } from "node:http";
|
||||||
import { tmpdir } from "node:os";
|
import { tmpdir } from "node:os";
|
||||||
import { join } from "node:path";
|
import { basename, join } from "node:path";
|
||||||
import { downloadImage, parseImageRef } from "./container-baby.js";
|
import { downloadImage, parseImageRef } from "./container-baby.js";
|
||||||
import { promisify } from "node:util";
|
import { promisify } from "node:util";
|
||||||
|
import { access, chmod, chown, link, mkdir } from "node:fs/promises";
|
||||||
const execFile = promisify(_execFile);
|
const execFile = promisify(_execFile);
|
||||||
|
|
||||||
const server = createServer(listener);
|
const server = createServer(listener);
|
||||||
|
@ -55,52 +57,115 @@ async function runVm(image) {
|
||||||
* @prop {boolean} is_read_only
|
* @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 {
|
class FirecrackerInstance {
|
||||||
proc;
|
vmid;
|
||||||
socketPath;
|
socketPath;
|
||||||
constructor() {
|
proc;
|
||||||
const vmid = nanoid();
|
/**
|
||||||
this.socketPath = join(tmpdir(), `firecracker-${vmid}.sock`);
|
* @param {{ firecrackerExe: string }} param0
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
constructor({ firecrackerExe }) {
|
||||||
|
this.vmid = jailerNanoid();
|
||||||
|
this.socketPath = join(this.chrootPath, `firecracker.sock`);
|
||||||
console.debug(this.socketPath);
|
console.debug(this.socketPath);
|
||||||
// TODO: jailer
|
|
||||||
this.proc = spawn(
|
this.proc = spawn(
|
||||||
"firecracker",
|
"jailer",
|
||||||
[
|
[
|
||||||
...["--api-sock", this.socketPath],
|
...["--id", this.vmid],
|
||||||
...["--level", "Debug"],
|
...["--exec-file", firecrackerExe],
|
||||||
...["--log-path", "/dev/stderr"],
|
...["--uid", "" + jailerUid],
|
||||||
"--show-level",
|
...["--gid", "" + jailerGid],
|
||||||
"--show-log-origin",
|
...["--chroot-base-dir", jailerDir],
|
||||||
|
"--",
|
||||||
|
...["--api-sock", "/firecracker.sock"],
|
||||||
|
// ...["--level", "Debug"],
|
||||||
|
// ...["--log-path", "/stderr.log"],
|
||||||
|
// "--show-level",
|
||||||
|
// "--show-log-origin",
|
||||||
],
|
],
|
||||||
{
|
{
|
||||||
stdio: "inherit",
|
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[] }}
|
* @param [opts] {{ drives?: Drive[] }}
|
||||||
*/
|
*/
|
||||||
static async run(opts) {
|
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
|
// TODO: retry until success
|
||||||
await delay(15);
|
await delay(15);
|
||||||
|
|
||||||
const { ifname, hostAddr, guestAddr } = await createNetworkInterface();
|
const { ifname, hostAddr, guestAddr } = await createNetworkInterface();
|
||||||
|
|
||||||
|
await self.hardlinkToChroot("../vmlinux.bin", 0o400);
|
||||||
await self.request("PUT", "/boot-source", {
|
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}`,
|
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",
|
drive_id: "rootfs",
|
||||||
path_on_host: "../rootfs.ext4",
|
path_on_host: "/rootfs.ext4",
|
||||||
is_root_device: true,
|
is_root_device: true,
|
||||||
// TODO: readonly
|
|
||||||
is_read_only: false,
|
is_read_only: false,
|
||||||
});
|
});
|
||||||
if (opts?.drives) {
|
if (opts?.drives) {
|
||||||
for (const drive of 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", {
|
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} method
|
||||||
* @param {string} path
|
* @param {string} path
|
||||||
|
|
|
@ -5,7 +5,7 @@
|
||||||
"description": "",
|
"description": "",
|
||||||
"main": "index.js",
|
"main": "index.js",
|
||||||
"scripts": {
|
"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": [],
|
"keywords": [],
|
||||||
"author": "",
|
"author": "",
|
||||||
|
@ -15,12 +15,14 @@
|
||||||
"@types/gunzip-maybe": "^1.4.0",
|
"@types/gunzip-maybe": "^1.4.0",
|
||||||
"@types/node": "^20.2.5",
|
"@types/node": "^20.2.5",
|
||||||
"@types/tar-stream": "^2.2.2",
|
"@types/tar-stream": "^2.2.2",
|
||||||
|
"@types/which": "^3.0.0",
|
||||||
"undici": "^5.22.1"
|
"undici": "^5.22.1"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"gunzip-maybe": "^1.4.2",
|
"gunzip-maybe": "^1.4.2",
|
||||||
"nanodelay": "^2.0.2",
|
"nanodelay": "^2.0.2",
|
||||||
"nanoid": "^4.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:
|
tar-stream:
|
||||||
specifier: ^3.0.0
|
specifier: ^3.0.0
|
||||||
version: 3.0.0
|
version: 3.0.0
|
||||||
|
which:
|
||||||
|
specifier: ^3.0.1
|
||||||
|
version: 3.0.1
|
||||||
|
|
||||||
devDependencies:
|
devDependencies:
|
||||||
'@tsconfig/node16':
|
'@tsconfig/node16':
|
||||||
|
@ -31,6 +34,9 @@ devDependencies:
|
||||||
'@types/tar-stream':
|
'@types/tar-stream':
|
||||||
specifier: ^2.2.2
|
specifier: ^2.2.2
|
||||||
version: 2.2.2
|
version: 2.2.2
|
||||||
|
'@types/which':
|
||||||
|
specifier: ^3.0.0
|
||||||
|
version: 3.0.0
|
||||||
undici:
|
undici:
|
||||||
specifier: ^5.22.1
|
specifier: ^5.22.1
|
||||||
version: 5.22.1
|
version: 5.22.1
|
||||||
|
@ -57,6 +63,10 @@ packages:
|
||||||
'@types/node': 20.2.5
|
'@types/node': 20.2.5
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
|
/@types/which@3.0.0:
|
||||||
|
resolution: {integrity: sha512-ASCxdbsrwNfSMXALlC3Decif9rwDMu+80KGp5zI2RLRotfMsTv7fHL8W8VDp24wymzDyIFudhUeSCugrgRFfHQ==}
|
||||||
|
dev: true
|
||||||
|
|
||||||
/abort-controller@3.0.0:
|
/abort-controller@3.0.0:
|
||||||
resolution: {integrity: sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==}
|
resolution: {integrity: sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==}
|
||||||
engines: {node: '>=6.5'}
|
engines: {node: '>=6.5'}
|
||||||
|
@ -170,6 +180,10 @@ packages:
|
||||||
resolution: {integrity: sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==}
|
resolution: {integrity: sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==}
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
|
/isexe@2.0.0:
|
||||||
|
resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==}
|
||||||
|
dev: false
|
||||||
|
|
||||||
/nanodelay@2.0.2:
|
/nanodelay@2.0.2:
|
||||||
resolution: {integrity: sha512-6AS5aCSXsjoxq2Jr9CdaAeT60yoYDOTp6po9ziqeOeY6vf6uTEHYSqWql6EFILrM3fEfXgkZ4KqE9L0rTm/wlA==}
|
resolution: {integrity: sha512-6AS5aCSXsjoxq2Jr9CdaAeT60yoYDOTp6po9ziqeOeY6vf6uTEHYSqWql6EFILrM3fEfXgkZ4KqE9L0rTm/wlA==}
|
||||||
engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0}
|
engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0}
|
||||||
|
@ -301,6 +315,14 @@ packages:
|
||||||
resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==}
|
resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==}
|
||||||
dev: false
|
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:
|
/wrappy@1.0.2:
|
||||||
resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==}
|
resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==}
|
||||||
dev: false
|
dev: false
|
||||||
|
|
|
@ -2,6 +2,8 @@
|
||||||
// TODO: sandboxear mkquashfs
|
// TODO: sandboxear mkquashfs
|
||||||
// TODO: fijarse como hace firecracker-containerd
|
// TODO: fijarse como hace firecracker-containerd
|
||||||
// TODO: fijarse como hace [ignite](https://github.com/weaveworks/ignite)
|
// 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 gunzip from "gunzip-maybe";
|
||||||
import { spawn } from "node:child_process";
|
import { spawn } from "node:child_process";
|
||||||
|
|
Loading…
Reference in a new issue