Compare commits
1 commit
i-love-ama
...
tar-stream
Author | SHA1 | Date | |
---|---|---|---|
ce19fa4ec9 |
26 changed files with 2161 additions and 1568 deletions
7
.gitignore
vendored
7
.gitignore
vendored
|
@ -1,5 +1,10 @@
|
||||||
rootfs.ext4
|
rootfs.ext4
|
||||||
|
rootfs.qcow2
|
||||||
|
fireactions
|
||||||
|
*.ext4
|
||||||
|
*.squashfs
|
||||||
|
*.config.json
|
||||||
vmlinux.bin
|
vmlinux.bin
|
||||||
node_modules/
|
node_modules/
|
||||||
|
|
||||||
build.cjs
|
*.log
|
||||||
|
|
184
agent/index.js
Normal file
184
agent/index.js
Normal file
|
@ -0,0 +1,184 @@
|
||||||
|
import { nanoid } from "nanoid";
|
||||||
|
// import { WebSocketServer } from "ws";
|
||||||
|
import { execFile as _execFile, spawn } from "node:child_process";
|
||||||
|
import { open, readFile, rm } from "node:fs/promises";
|
||||||
|
import { createServer } from "node:http";
|
||||||
|
import { tmpdir } from "node:os";
|
||||||
|
import { join } from "node:path";
|
||||||
|
import { promisify } from "node:util";
|
||||||
|
const execFile = promisify(_execFile);
|
||||||
|
// const spawn = promisify(_spawn);
|
||||||
|
|
||||||
|
const secret = await parseSecret();
|
||||||
|
|
||||||
|
// const wsServer = new WebSocketServer({ noServer: true });
|
||||||
|
const server = createServer(async (req, res) => {
|
||||||
|
try {
|
||||||
|
await handler(req, res);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error manejando", req.url, error);
|
||||||
|
res.destroy();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
/** @type {Map<string, {listeners: ((data: Buffer | null) => void)[], prev: Buffer, statusCode: null | number}>} */
|
||||||
|
let runs = new Map();
|
||||||
|
|
||||||
|
// server.on("upgrade", (req, socket, head) => {
|
||||||
|
// const url = getUrl(req)
|
||||||
|
// let matches;
|
||||||
|
// if ((matches = url.pathname.match(runWebSocketRegexp))) {
|
||||||
|
// const runId = matches[1];
|
||||||
|
// if (!listeners.has(runId)) {
|
||||||
|
// return
|
||||||
|
// }
|
||||||
|
// wsServer.handleUpgrade(req, socket,head,(client,request)=>{
|
||||||
|
|
||||||
|
// })
|
||||||
|
// }
|
||||||
|
// });
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {string} runId
|
||||||
|
* @param {Buffer} chunk
|
||||||
|
*/
|
||||||
|
function writeToRun(runId, chunk) {
|
||||||
|
const run = runs.get(runId);
|
||||||
|
if (!run) throw new Error(`runId ${runId} doesn't exist`);
|
||||||
|
run.prev = Buffer.concat([run.prev, chunk]);
|
||||||
|
for (const listener of run.listeners) {
|
||||||
|
listener(chunk);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {string} runId
|
||||||
|
*/
|
||||||
|
function closeRun(runId) {
|
||||||
|
const run = runs.get(runId);
|
||||||
|
if (!run) throw new Error(`runId ${runId} doesn't exist`);
|
||||||
|
for (const listener of run.listeners) {
|
||||||
|
listener(null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const runWebSocketRegexp = /^\/run\/(.+)$/;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {import("http").IncomingMessage} req
|
||||||
|
* @param {import("http").ServerResponse<import("http").IncomingMessage> & { req: import("http").IncomingMessage; }} res
|
||||||
|
*/
|
||||||
|
async function handler(req, res) {
|
||||||
|
// prettier-ignore
|
||||||
|
const url = getUrl(req);
|
||||||
|
auth(secret, req, res);
|
||||||
|
let matches;
|
||||||
|
if (url.pathname === "/hello") {
|
||||||
|
if (req.method === "GET") {
|
||||||
|
return res.writeHead(200).end("Ey!");
|
||||||
|
} else return res.writeHead(405).end("method not allowed");
|
||||||
|
} else if (url.pathname === "/run") {
|
||||||
|
if (req.method === "POST") {
|
||||||
|
const runId = nanoid();
|
||||||
|
runs.set(runId, {
|
||||||
|
listeners: [],
|
||||||
|
prev: Buffer.from([]),
|
||||||
|
statusCode: null,
|
||||||
|
});
|
||||||
|
const execPath = join(tmpdir(), "fireactions-agent-exec-" + nanoid());
|
||||||
|
{
|
||||||
|
const execFile = await open(execPath, "w", 0o700);
|
||||||
|
await new Promise((resolve, reject) =>
|
||||||
|
req.pipe(execFile.createWriteStream()).addListener("finish", () => {
|
||||||
|
resolve(void 0);
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
const proc = spawn(execPath, [], {});
|
||||||
|
proc.stdout.on("data", async (/** @type {Buffer} */ chunk) => {
|
||||||
|
writeToRun(runId, chunk);
|
||||||
|
});
|
||||||
|
proc.stderr.on("data", async (/** @type {Buffer} */ chunk) => {
|
||||||
|
writeToRun(runId, chunk);
|
||||||
|
});
|
||||||
|
proc.on("close", async (code) => {
|
||||||
|
const run = runs.get(runId);
|
||||||
|
if (!run) return panic();
|
||||||
|
runs.set(runId, { ...run, statusCode: code });
|
||||||
|
closeRun(runId);
|
||||||
|
await rm(execPath);
|
||||||
|
});
|
||||||
|
return res.writeHead(200).end(JSON.stringify({ runId }));
|
||||||
|
} else return res.writeHead(405).end("method not allowed");
|
||||||
|
} else if ((matches = url.pathname.match(runWebSocketRegexp))) {
|
||||||
|
const runId = matches[1];
|
||||||
|
const run = runs.get(runId);
|
||||||
|
if (!run) {
|
||||||
|
return res.writeHead(404).end("run not found");
|
||||||
|
}
|
||||||
|
res.writeHead(200, {
|
||||||
|
"content-type": "text/event-stream",
|
||||||
|
Connection: "keep-alive",
|
||||||
|
"Cache-Control": "no-cache",
|
||||||
|
"Access-Control-Allow-Origin": "*",
|
||||||
|
});
|
||||||
|
const listener = (/** @type {Buffer | null} */ data) => {
|
||||||
|
if (data === null) return res.end();
|
||||||
|
res.write(
|
||||||
|
"event: stdsmth\n" + `data: ${JSON.stringify(data.toString())}\n\n`
|
||||||
|
);
|
||||||
|
};
|
||||||
|
listener(run.prev);
|
||||||
|
// TODO: remove listener when request closed
|
||||||
|
runs.set(runId, { ...run, listeners: [...run.listeners, listener] });
|
||||||
|
} else {
|
||||||
|
return res.writeHead(404).end("not found");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @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;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @returns {Promise<string>}
|
||||||
|
*/
|
||||||
|
async function parseSecret() {
|
||||||
|
const cmdline = await readFile("/proc/cmdline", "utf-8");
|
||||||
|
for (const opt of cmdline.split(" ")) {
|
||||||
|
const [key, value] = opt.split("=");
|
||||||
|
if (key === "fireactions.secret") {
|
||||||
|
if (value.length < 5) throw new Error("valor muy corto");
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
throw new Error("no hay secreto");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {string} secret
|
||||||
|
* @param {import("http").IncomingMessage} req
|
||||||
|
* @param {import("http").ServerResponse<import("http").IncomingMessage> & { req: import("http").IncomingMessage; }} res
|
||||||
|
*/
|
||||||
|
function auth(secret, req, res) {
|
||||||
|
const url = getUrl(req);
|
||||||
|
const auth = req.headers.authorization
|
||||||
|
? req.headers.authorization.slice("Bearer ".length)
|
||||||
|
: url.searchParams.get("token");
|
||||||
|
if (auth !== secret) {
|
||||||
|
res.writeHead(401).end("wrong secret");
|
||||||
|
throw new Error("unauthorized");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
server.listen("8080");
|
||||||
|
|
||||||
|
function panic() {
|
||||||
|
throw new Error("Function not implemented.");
|
||||||
|
}
|
22
agent/package.json
Normal file
22
agent/package.json
Normal file
|
@ -0,0 +1,22 @@
|
||||||
|
{
|
||||||
|
"name": "agent",
|
||||||
|
"type": "module",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"description": "",
|
||||||
|
"main": "index.js",
|
||||||
|
"scripts": {
|
||||||
|
"test": "echo \"Error: no test specified\" && exit 1"
|
||||||
|
},
|
||||||
|
"keywords": [],
|
||||||
|
"author": "",
|
||||||
|
"license": "ISC",
|
||||||
|
"devDependencies": {
|
||||||
|
"@tsconfig/node16": "^1.0.4",
|
||||||
|
"@types/node": "^20.2.5",
|
||||||
|
"@types/ws": "^8.5.4"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"nanoid": "^4.0.2",
|
||||||
|
"ws": "^8.13.0"
|
||||||
|
}
|
||||||
|
}
|
59
agent/pnpm-lock.yaml
Normal file
59
agent/pnpm-lock.yaml
Normal file
|
@ -0,0 +1,59 @@
|
||||||
|
lockfileVersion: '6.1'
|
||||||
|
|
||||||
|
settings:
|
||||||
|
autoInstallPeers: true
|
||||||
|
excludeLinksFromLockfile: false
|
||||||
|
|
||||||
|
dependencies:
|
||||||
|
nanoid:
|
||||||
|
specifier: ^4.0.2
|
||||||
|
version: 4.0.2
|
||||||
|
ws:
|
||||||
|
specifier: ^8.13.0
|
||||||
|
version: 8.13.0
|
||||||
|
|
||||||
|
devDependencies:
|
||||||
|
'@tsconfig/node16':
|
||||||
|
specifier: ^1.0.4
|
||||||
|
version: 1.0.4
|
||||||
|
'@types/node':
|
||||||
|
specifier: ^20.2.5
|
||||||
|
version: 20.2.5
|
||||||
|
'@types/ws':
|
||||||
|
specifier: ^8.5.4
|
||||||
|
version: 8.5.4
|
||||||
|
|
||||||
|
packages:
|
||||||
|
|
||||||
|
/@tsconfig/node16@1.0.4:
|
||||||
|
resolution: {integrity: sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==}
|
||||||
|
dev: true
|
||||||
|
|
||||||
|
/@types/node@20.2.5:
|
||||||
|
resolution: {integrity: sha512-JJulVEQXmiY9Px5axXHeYGLSjhkZEnD+MDPDGbCbIAbMslkKwmygtZFy1X6s/075Yo94sf8GuSlFfPzysQrWZQ==}
|
||||||
|
dev: true
|
||||||
|
|
||||||
|
/@types/ws@8.5.4:
|
||||||
|
resolution: {integrity: sha512-zdQDHKUgcX/zBc4GrwsE/7dVdAD8JR4EuiAXiiUhhfyIJXXb2+PrGshFyeXWQPMmmZ2XxgaqclgpIC7eTXc1mg==}
|
||||||
|
dependencies:
|
||||||
|
'@types/node': 20.2.5
|
||||||
|
dev: true
|
||||||
|
|
||||||
|
/nanoid@4.0.2:
|
||||||
|
resolution: {integrity: sha512-7ZtY5KTCNheRGfEFxnedV5zFiORN1+Y1N6zvPTnHQd8ENUvfaDBeuJDZb2bN/oXwXxu3qkTXDzy57W5vAmDTBw==}
|
||||||
|
engines: {node: ^14 || ^16 || >=18}
|
||||||
|
hasBin: true
|
||||||
|
dev: false
|
||||||
|
|
||||||
|
/ws@8.13.0:
|
||||||
|
resolution: {integrity: sha512-x9vcZYTrFPC7aSIbj7sRCYo7L/Xb8Iy+pW0ng0wt2vCJv7M9HOMy0UoN3rr+IFC7hb7vXoqS+P9ktyLLLhO+LA==}
|
||||||
|
engines: {node: '>=10.0.0'}
|
||||||
|
peerDependencies:
|
||||||
|
bufferutil: ^4.0.1
|
||||||
|
utf-8-validate: '>=5.0.2'
|
||||||
|
peerDependenciesMeta:
|
||||||
|
bufferutil:
|
||||||
|
optional: true
|
||||||
|
utf-8-validate:
|
||||||
|
optional: true
|
||||||
|
dev: false
|
8
agent/tsconfig.json
Normal file
8
agent/tsconfig.json
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
{
|
||||||
|
"extends": "@tsconfig/node16/tsconfig.json",
|
||||||
|
"exclude": ["build.js/", "build/"],
|
||||||
|
"compilerOptions": {
|
||||||
|
"allowJs": true,
|
||||||
|
"checkJs": true
|
||||||
|
}
|
||||||
|
}
|
18
fcnet.conflist
Normal file
18
fcnet.conflist
Normal file
|
@ -0,0 +1,18 @@
|
||||||
|
{
|
||||||
|
"name": "fcnet",
|
||||||
|
"cniVersion": "0.4.0",
|
||||||
|
"plugins": [
|
||||||
|
{
|
||||||
|
"type": "ptp",
|
||||||
|
"ipMasq": true,
|
||||||
|
"ipam": {
|
||||||
|
"type": "host-local",
|
||||||
|
"subnet": "192.168.127.0/24",
|
||||||
|
"resolvConf": "/etc/resolv.conf"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "tc-redirect-tap"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
51
go.mod
Normal file
51
go.mod
Normal file
|
@ -0,0 +1,51 @@
|
||||||
|
module gitea.nulo.in/Nulo/fireactions
|
||||||
|
|
||||||
|
go 1.20
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/PuerkitoBio/purell v1.1.1 // indirect
|
||||||
|
github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 // indirect
|
||||||
|
github.com/asaskevich/govalidator v0.0.0-20210307081110-f21760c49a8d // indirect
|
||||||
|
github.com/containerd/fifo v1.0.0 // indirect
|
||||||
|
github.com/containernetworking/cni v1.0.1 // indirect
|
||||||
|
github.com/containernetworking/plugins v1.0.1 // indirect
|
||||||
|
github.com/firecracker-microvm/firecracker-go-sdk v1.0.0 // indirect
|
||||||
|
github.com/go-openapi/analysis v0.21.2 // indirect
|
||||||
|
github.com/go-openapi/errors v0.20.2 // indirect
|
||||||
|
github.com/go-openapi/jsonpointer v0.19.5 // indirect
|
||||||
|
github.com/go-openapi/jsonreference v0.19.6 // indirect
|
||||||
|
github.com/go-openapi/loads v0.21.1 // indirect
|
||||||
|
github.com/go-openapi/runtime v0.24.0 // indirect
|
||||||
|
github.com/go-openapi/spec v0.20.4 // indirect
|
||||||
|
github.com/go-openapi/strfmt v0.21.2 // indirect
|
||||||
|
github.com/go-openapi/swag v0.21.1 // indirect
|
||||||
|
github.com/go-openapi/validate v0.22.0 // indirect
|
||||||
|
github.com/go-stack/stack v1.8.1 // indirect
|
||||||
|
github.com/golang-jwt/jwt v3.2.2+incompatible // indirect
|
||||||
|
github.com/google/uuid v1.3.0 // indirect
|
||||||
|
github.com/hashicorp/errwrap v1.0.0 // indirect
|
||||||
|
github.com/hashicorp/go-multierror v1.1.1 // indirect
|
||||||
|
github.com/jaevor/go-nanoid v1.3.0 // indirect
|
||||||
|
github.com/josharian/intern v1.0.0 // indirect
|
||||||
|
github.com/labstack/echo/v4 v4.10.2 // indirect
|
||||||
|
github.com/labstack/gommon v0.4.0 // indirect
|
||||||
|
github.com/mailru/easyjson v0.7.7 // indirect
|
||||||
|
github.com/mattn/go-colorable v0.1.13 // indirect
|
||||||
|
github.com/mattn/go-isatty v0.0.17 // indirect
|
||||||
|
github.com/mitchellh/mapstructure v1.4.3 // indirect
|
||||||
|
github.com/oklog/ulid v1.3.1 // indirect
|
||||||
|
github.com/opentracing/opentracing-go v1.2.0 // indirect
|
||||||
|
github.com/pkg/errors v0.9.1 // indirect
|
||||||
|
github.com/sirupsen/logrus v1.8.1 // indirect
|
||||||
|
github.com/valyala/bytebufferpool v1.0.0 // indirect
|
||||||
|
github.com/valyala/fasttemplate v1.2.2 // indirect
|
||||||
|
github.com/vishvananda/netlink v1.1.1-0.20210330154013-f5de75959ad5 // indirect
|
||||||
|
github.com/vishvananda/netns v0.0.0-20210104183010-2eb08e3e575f // indirect
|
||||||
|
go.mongodb.org/mongo-driver v1.8.3 // indirect
|
||||||
|
golang.org/x/crypto v0.6.0 // indirect
|
||||||
|
golang.org/x/net v0.7.0 // indirect
|
||||||
|
golang.org/x/sys v0.5.0 // indirect
|
||||||
|
golang.org/x/text v0.7.0 // indirect
|
||||||
|
golang.org/x/time v0.3.0 // indirect
|
||||||
|
gopkg.in/yaml.v2 v2.4.0 // indirect
|
||||||
|
)
|
2
js/.gitignore
vendored
Normal file
2
js/.gitignore
vendored
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
# compiled typescript
|
||||||
|
container-baby.js
|
198
js/container-baby.ts
Normal file
198
js/container-baby.ts
Normal file
|
@ -0,0 +1,198 @@
|
||||||
|
import { execFile as _execFile } from "node:child_process";
|
||||||
|
import { mkdtemp, open, rm } from "node:fs/promises";
|
||||||
|
import { join } from "node:path";
|
||||||
|
import { Writable } from "node:stream";
|
||||||
|
import { promisify } from "node:util";
|
||||||
|
import { tar2squashfs } from "./tar2squashfs.js";
|
||||||
|
const execFile = promisify(_execFile);
|
||||||
|
|
||||||
|
type RegistryName = string;
|
||||||
|
type RegistrySecret = string;
|
||||||
|
let tokenCache = new Map<RegistryName, Promise<RegistrySecret>>();
|
||||||
|
|
||||||
|
const tmp = await mkdtemp("sexy-chambelan-");
|
||||||
|
try {
|
||||||
|
console.debug(tmp);
|
||||||
|
|
||||||
|
const image = "gitea.nulo.in/nulo/zulip-checkin-cyborg";
|
||||||
|
const tag = "latest";
|
||||||
|
const arch = "amd64";
|
||||||
|
|
||||||
|
const manifest = await getContainerManifest(image, tag);
|
||||||
|
if (
|
||||||
|
!manifest.layers.every(
|
||||||
|
(layer) =>
|
||||||
|
layer.mediaType === "application/vnd.oci.image.layer.v1.tar" ||
|
||||||
|
layer.mediaType === "application/vnd.oci.image.layer.v1.tar+gzip"
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
throw new Error("Unsupported layer");
|
||||||
|
}
|
||||||
|
console.debug(manifest);
|
||||||
|
const layers = await Promise.all(
|
||||||
|
manifest.layers.map(async (layer) => {
|
||||||
|
const path = join(tmp, layer.digest);
|
||||||
|
await downloadBlob(image, layer.digest, path);
|
||||||
|
return { layer, path };
|
||||||
|
})
|
||||||
|
);
|
||||||
|
const squashfs = `${image.replaceAll("/", "%")}.squashfs`;
|
||||||
|
await rm(squashfs, { force: true });
|
||||||
|
await tar2squashfs(
|
||||||
|
layers.map(async ({ path }) => {
|
||||||
|
const f = await open(path);
|
||||||
|
return f.createReadStream();
|
||||||
|
}),
|
||||||
|
squashfs
|
||||||
|
);
|
||||||
|
await downloadBlob(
|
||||||
|
image,
|
||||||
|
manifest.config.digest,
|
||||||
|
`${image.replaceAll("/", "%")}.config.json`
|
||||||
|
);
|
||||||
|
process.exit(1);
|
||||||
|
} finally {
|
||||||
|
await rm(tmp, { recursive: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
async function _getToken(registryUrl: string): Promise<RegistrySecret> {
|
||||||
|
const res = await fetch(`${registryUrl}/token`);
|
||||||
|
const json = await res.json();
|
||||||
|
if (
|
||||||
|
!json ||
|
||||||
|
typeof json !== "object" ||
|
||||||
|
!("token" in json) ||
|
||||||
|
typeof json.token !== "string"
|
||||||
|
)
|
||||||
|
throw new Error("Unexpected");
|
||||||
|
return json.token;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function call(registryName: string, path: string, init: RequestInit) {
|
||||||
|
const registryUrl = `https://${registryName}/v2`;
|
||||||
|
let tokenPromise = tokenCache.get(registryName);
|
||||||
|
if (!tokenPromise) {
|
||||||
|
tokenPromise = _getToken(registryUrl);
|
||||||
|
tokenCache.set(registryName, tokenPromise);
|
||||||
|
}
|
||||||
|
const token = await tokenPromise;
|
||||||
|
console.debug(path);
|
||||||
|
const res = await fetch(`${registryUrl}${path}`, {
|
||||||
|
...init,
|
||||||
|
headers: {
|
||||||
|
...init.headers,
|
||||||
|
Authorization: `Bearer ${token}`,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
if (!res.ok) throw new Error(res.statusText);
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
|
||||||
|
type ManifestPointer = {
|
||||||
|
mediaType: "application/vnd.oci.image.manifest.v1+json";
|
||||||
|
digest: string;
|
||||||
|
size: number;
|
||||||
|
platform: {
|
||||||
|
architecture: "amd64" | string;
|
||||||
|
os: string;
|
||||||
|
};
|
||||||
|
annotations?: { [k: string]: string };
|
||||||
|
};
|
||||||
|
|
||||||
|
// https://github.com/opencontainers/image-spec/blob/v1.0.1/image-index.md
|
||||||
|
type ManifestIndex = {
|
||||||
|
mediaType: "application/vnd.oci.image.index.v1+json";
|
||||||
|
schemaVersion: 2;
|
||||||
|
manifests: ManifestPointer[];
|
||||||
|
};
|
||||||
|
|
||||||
|
function getImageParts(image: string): [string, string] {
|
||||||
|
const parts = image.split("/");
|
||||||
|
const registryName = parts[0];
|
||||||
|
const imgPath = parts.slice(1).join("/");
|
||||||
|
return [registryName, imgPath];
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getContainerManifest(image: string, tag: string) {
|
||||||
|
const index = await getManifestIndex(image, tag);
|
||||||
|
// badly behaved registries will return whatever they want
|
||||||
|
if (
|
||||||
|
(index.mediaType as string) === "application/vnd.oci.image.manifest.v1+json"
|
||||||
|
) {
|
||||||
|
return index as unknown as Manifest;
|
||||||
|
}
|
||||||
|
const arch = "amd64";
|
||||||
|
const ptr = chooseManifest(index, arch);
|
||||||
|
if (!ptr)
|
||||||
|
throw new Error(`Image ${image}:${tag} doesn't exist for arch ${arch}`);
|
||||||
|
const manifest = await getManifest(image, ptr);
|
||||||
|
return manifest;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getManifestIndex(
|
||||||
|
image: string,
|
||||||
|
tag: string
|
||||||
|
): Promise<ManifestIndex> {
|
||||||
|
const [registryName, imgPath] = getImageParts(image);
|
||||||
|
|
||||||
|
const res = await call(registryName, `/${imgPath}/manifests/${tag}`, {
|
||||||
|
headers: {
|
||||||
|
Accept: "application/vnd.oci.image.index.v1+json",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const json = await res.json();
|
||||||
|
return json as ManifestIndex;
|
||||||
|
}
|
||||||
|
|
||||||
|
function chooseManifest(list: ManifestIndex, arch: string) {
|
||||||
|
console.debug(list);
|
||||||
|
return list.manifests.find(
|
||||||
|
(m) => m.platform.architecture === arch && m.platform.os === "linux"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getBlob(image: string, digest: string): Promise<Response> {
|
||||||
|
const [registryName, imgPath] = getImageParts(image);
|
||||||
|
return await call(registryName, `/${imgPath}/blobs/${digest}`, {});
|
||||||
|
}
|
||||||
|
async function jsonBlob<T>(image: string, digest: string): Promise<T> {
|
||||||
|
const res = await getBlob(image, digest);
|
||||||
|
return (await res.json()) as T;
|
||||||
|
}
|
||||||
|
async function downloadBlob(
|
||||||
|
image: string,
|
||||||
|
digest: string,
|
||||||
|
outputPath: string
|
||||||
|
): Promise<void> {
|
||||||
|
const res = await getBlob(image, digest);
|
||||||
|
const file = await open(outputPath, "w");
|
||||||
|
await res.body!.pipeTo(Writable.toWeb(file.createWriteStream()));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// https://github.com/opencontainers/image-spec/blob/v1.0.1/descriptor.md#properties
|
||||||
|
type Descriptor = {
|
||||||
|
digest: string;
|
||||||
|
size: number;
|
||||||
|
urls?: string[];
|
||||||
|
annotations?: { [k: string]: string };
|
||||||
|
};
|
||||||
|
|
||||||
|
// https://github.com/opencontainers/image-spec/blob/v1.0.1/manifest.md
|
||||||
|
type Manifest = {
|
||||||
|
mediaType: "application/vnd.oci.image.manifest.v1+json";
|
||||||
|
config: {
|
||||||
|
mediaType: "application/vnd.oci.image.config.v1+json";
|
||||||
|
} & Descriptor;
|
||||||
|
layers: ({
|
||||||
|
mediaType:
|
||||||
|
| "application/vnd.oci.image.layer.v1.tar"
|
||||||
|
| "application/vnd.oci.image.layer.v1.tar+gzip"
|
||||||
|
| "application/vnd.oci.image.layer.nondistributable.v1.tar"
|
||||||
|
| "application/vnd.oci.image.layer.nondistributable.v1.tar+gzip";
|
||||||
|
} & Descriptor)[];
|
||||||
|
};
|
||||||
|
|
||||||
|
async function getManifest(image: string, ptr: ManifestPointer) {
|
||||||
|
return await jsonBlob<Manifest>(image, ptr.digest);
|
||||||
|
}
|
0
server/fetch-hack.d.ts → js/fetch-hack.d.ts
vendored
0
server/fetch-hack.d.ts → js/fetch-hack.d.ts
vendored
143
js/index.js
Normal file
143
js/index.js
Normal file
|
@ -0,0 +1,143 @@
|
||||||
|
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");
|
|
@ -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 env PATH=\"$PATH:$HOME/.local/bin\" node --enable-source-maps build.cjs"
|
"test": "echo \"Error: no test specified\" && exit 1"
|
||||||
},
|
},
|
||||||
"keywords": [],
|
"keywords": [],
|
||||||
"author": "",
|
"author": "",
|
||||||
|
@ -15,14 +15,12 @@
|
||||||
"@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.1.2"
|
||||||
"which": "^3.0.1"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -14,12 +14,12 @@ dependencies:
|
||||||
nanoid:
|
nanoid:
|
||||||
specifier: ^4.0.2
|
specifier: ^4.0.2
|
||||||
version: 4.0.2
|
version: 4.0.2
|
||||||
|
tar:
|
||||||
|
specifier: ^6.1.15
|
||||||
|
version: 6.1.15
|
||||||
tar-stream:
|
tar-stream:
|
||||||
specifier: ^3.0.0
|
specifier: ^3.1.2
|
||||||
version: 3.0.0
|
version: 3.1.2
|
||||||
which:
|
|
||||||
specifier: ^3.0.1
|
|
||||||
version: 3.0.1
|
|
||||||
|
|
||||||
devDependencies:
|
devDependencies:
|
||||||
'@tsconfig/node16':
|
'@tsconfig/node16':
|
||||||
|
@ -31,12 +31,12 @@ devDependencies:
|
||||||
'@types/node':
|
'@types/node':
|
||||||
specifier: ^20.2.5
|
specifier: ^20.2.5
|
||||||
version: 20.2.5
|
version: 20.2.5
|
||||||
|
'@types/tar':
|
||||||
|
specifier: ^6.1.5
|
||||||
|
version: 6.1.5
|
||||||
'@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
|
||||||
|
@ -63,33 +63,17 @@ packages:
|
||||||
'@types/node': 20.2.5
|
'@types/node': 20.2.5
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
/@types/which@3.0.0:
|
/@types/tar@6.1.5:
|
||||||
resolution: {integrity: sha512-ASCxdbsrwNfSMXALlC3Decif9rwDMu+80KGp5zI2RLRotfMsTv7fHL8W8VDp24wymzDyIFudhUeSCugrgRFfHQ==}
|
resolution: {integrity: sha512-qm2I/RlZij5RofuY7vohTpYNaYcrSQlN2MyjucQc7ZweDwaEWkdN/EeNh6e9zjK6uEm6PwjdMXkcj05BxZdX1Q==}
|
||||||
dev: true
|
|
||||||
|
|
||||||
/abort-controller@3.0.0:
|
|
||||||
resolution: {integrity: sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==}
|
|
||||||
engines: {node: '>=6.5'}
|
|
||||||
dependencies:
|
dependencies:
|
||||||
event-target-shim: 5.0.1
|
'@types/node': 20.2.5
|
||||||
dev: false
|
minipass: 4.2.8
|
||||||
|
dev: true
|
||||||
|
|
||||||
/b4a@1.6.4:
|
/b4a@1.6.4:
|
||||||
resolution: {integrity: sha512-fpWrvyVHEKyeEvbKZTVOeZF3VSKKWtJxFIxX/jaVPf+cLbGUSitjb49pHLqPV2BUNNZ0LcoeEGfE/YCpyDYHIw==}
|
resolution: {integrity: sha512-fpWrvyVHEKyeEvbKZTVOeZF3VSKKWtJxFIxX/jaVPf+cLbGUSitjb49pHLqPV2BUNNZ0LcoeEGfE/YCpyDYHIw==}
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
/base64-js@1.5.1:
|
|
||||||
resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==}
|
|
||||||
dev: false
|
|
||||||
|
|
||||||
/bl@6.0.2:
|
|
||||||
resolution: {integrity: sha512-/ivXMGCGDI0EB4JI4zCqppp79j03vUgZz/zakw7TworE2NVjIuPxpL1Ti0InSsarKqFG5NLFreCBcCCSjtrTQw==}
|
|
||||||
dependencies:
|
|
||||||
buffer: 6.0.3
|
|
||||||
inherits: 2.0.4
|
|
||||||
readable-stream: 4.4.0
|
|
||||||
dev: false
|
|
||||||
|
|
||||||
/browserify-zlib@0.1.4:
|
/browserify-zlib@0.1.4:
|
||||||
resolution: {integrity: sha512-19OEpq7vWgsH6WkvkBJQDFvJS1uPcbFOQ4v9CU839dO+ZZXUZO6XpE6hNCqvlIIj+4fZvRiJ6DsAQ382GwiyTQ==}
|
resolution: {integrity: sha512-19OEpq7vWgsH6WkvkBJQDFvJS1uPcbFOQ4v9CU839dO+ZZXUZO6XpE6hNCqvlIIj+4fZvRiJ6DsAQ382GwiyTQ==}
|
||||||
dependencies:
|
dependencies:
|
||||||
|
@ -100,13 +84,6 @@ packages:
|
||||||
resolution: {integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==}
|
resolution: {integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==}
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
/buffer@6.0.3:
|
|
||||||
resolution: {integrity: sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==}
|
|
||||||
dependencies:
|
|
||||||
base64-js: 1.5.1
|
|
||||||
ieee754: 1.2.1
|
|
||||||
dev: false
|
|
||||||
|
|
||||||
/busboy@1.6.0:
|
/busboy@1.6.0:
|
||||||
resolution: {integrity: sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==}
|
resolution: {integrity: sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==}
|
||||||
engines: {node: '>=10.16.0'}
|
engines: {node: '>=10.16.0'}
|
||||||
|
@ -114,6 +91,11 @@ packages:
|
||||||
streamsearch: 1.1.0
|
streamsearch: 1.1.0
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
|
/chownr@2.0.0:
|
||||||
|
resolution: {integrity: sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==}
|
||||||
|
engines: {node: '>=10'}
|
||||||
|
dev: false
|
||||||
|
|
||||||
/core-util-is@1.0.3:
|
/core-util-is@1.0.3:
|
||||||
resolution: {integrity: sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==}
|
resolution: {integrity: sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==}
|
||||||
dev: false
|
dev: false
|
||||||
|
@ -133,20 +115,17 @@ packages:
|
||||||
once: 1.4.0
|
once: 1.4.0
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
/event-target-shim@5.0.1:
|
|
||||||
resolution: {integrity: sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==}
|
|
||||||
engines: {node: '>=6'}
|
|
||||||
dev: false
|
|
||||||
|
|
||||||
/events@3.3.0:
|
|
||||||
resolution: {integrity: sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==}
|
|
||||||
engines: {node: '>=0.8.x'}
|
|
||||||
dev: false
|
|
||||||
|
|
||||||
/fast-fifo@1.2.0:
|
/fast-fifo@1.2.0:
|
||||||
resolution: {integrity: sha512-NcvQXt7Cky1cNau15FWy64IjuO8X0JijhTBBrJj1YlxlDfRkJXNaK9RFUjwpfDPzMdv7wB38jr53l9tkNLxnWg==}
|
resolution: {integrity: sha512-NcvQXt7Cky1cNau15FWy64IjuO8X0JijhTBBrJj1YlxlDfRkJXNaK9RFUjwpfDPzMdv7wB38jr53l9tkNLxnWg==}
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
|
/fs-minipass@2.1.0:
|
||||||
|
resolution: {integrity: sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==}
|
||||||
|
engines: {node: '>= 8'}
|
||||||
|
dependencies:
|
||||||
|
minipass: 3.3.6
|
||||||
|
dev: false
|
||||||
|
|
||||||
/gunzip-maybe@1.4.2:
|
/gunzip-maybe@1.4.2:
|
||||||
resolution: {integrity: sha512-4haO1M4mLO91PW57BMsDFf75UmwoRX0GkdD+Faw+Lr+r/OZrOCS0pIBwOL1xCKQqnQzbNFGgK2V2CpBUPeFNTw==}
|
resolution: {integrity: sha512-4haO1M4mLO91PW57BMsDFf75UmwoRX0GkdD+Faw+Lr+r/OZrOCS0pIBwOL1xCKQqnQzbNFGgK2V2CpBUPeFNTw==}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
|
@ -159,10 +138,6 @@ packages:
|
||||||
through2: 2.0.5
|
through2: 2.0.5
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
/ieee754@1.2.1:
|
|
||||||
resolution: {integrity: sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==}
|
|
||||||
dev: false
|
|
||||||
|
|
||||||
/inherits@2.0.4:
|
/inherits@2.0.4:
|
||||||
resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==}
|
resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==}
|
||||||
dev: false
|
dev: false
|
||||||
|
@ -180,8 +155,35 @@ packages:
|
||||||
resolution: {integrity: sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==}
|
resolution: {integrity: sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==}
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
/isexe@2.0.0:
|
/minipass@3.3.6:
|
||||||
resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==}
|
resolution: {integrity: sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==}
|
||||||
|
engines: {node: '>=8'}
|
||||||
|
dependencies:
|
||||||
|
yallist: 4.0.0
|
||||||
|
dev: false
|
||||||
|
|
||||||
|
/minipass@4.2.8:
|
||||||
|
resolution: {integrity: sha512-fNzuVyifolSLFL4NzpF+wEF4qrgqaaKX0haXPQEdQ7NKAN+WecoKMHV09YcuL/DHxrUsYQOK3MiuDf7Ip2OXfQ==}
|
||||||
|
engines: {node: '>=8'}
|
||||||
|
dev: true
|
||||||
|
|
||||||
|
/minipass@5.0.0:
|
||||||
|
resolution: {integrity: sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ==}
|
||||||
|
engines: {node: '>=8'}
|
||||||
|
dev: false
|
||||||
|
|
||||||
|
/minizlib@2.1.2:
|
||||||
|
resolution: {integrity: sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==}
|
||||||
|
engines: {node: '>= 8'}
|
||||||
|
dependencies:
|
||||||
|
minipass: 3.3.6
|
||||||
|
yallist: 4.0.0
|
||||||
|
dev: false
|
||||||
|
|
||||||
|
/mkdirp@1.0.4:
|
||||||
|
resolution: {integrity: sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==}
|
||||||
|
engines: {node: '>=10'}
|
||||||
|
hasBin: true
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
/nanodelay@2.0.2:
|
/nanodelay@2.0.2:
|
||||||
|
@ -217,11 +219,6 @@ packages:
|
||||||
resolution: {integrity: sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==}
|
resolution: {integrity: sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==}
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
/process@0.11.10:
|
|
||||||
resolution: {integrity: sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==}
|
|
||||||
engines: {node: '>= 0.6.0'}
|
|
||||||
dev: false
|
|
||||||
|
|
||||||
/pump@2.0.1:
|
/pump@2.0.1:
|
||||||
resolution: {integrity: sha512-ruPMNRkN3MHP1cWJc9OWr+T/xDP0jhXYCLfJcBuX54hhfIBnaQmAUMfDcG4DM5UMWByBbJY69QSphm3jtDKIkA==}
|
resolution: {integrity: sha512-ruPMNRkN3MHP1cWJc9OWr+T/xDP0jhXYCLfJcBuX54hhfIBnaQmAUMfDcG4DM5UMWByBbJY69QSphm3jtDKIkA==}
|
||||||
dependencies:
|
dependencies:
|
||||||
|
@ -253,16 +250,6 @@ packages:
|
||||||
util-deprecate: 1.0.2
|
util-deprecate: 1.0.2
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
/readable-stream@4.4.0:
|
|
||||||
resolution: {integrity: sha512-kDMOq0qLtxV9f/SQv522h8cxZBqNZXuXNyjyezmfAAuribMyVXziljpQ/uQhfE1XLg2/TLTW2DsnoE4VAi/krg==}
|
|
||||||
engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
|
|
||||||
dependencies:
|
|
||||||
abort-controller: 3.0.0
|
|
||||||
buffer: 6.0.3
|
|
||||||
events: 3.3.0
|
|
||||||
process: 0.11.10
|
|
||||||
dev: false
|
|
||||||
|
|
||||||
/safe-buffer@5.1.2:
|
/safe-buffer@5.1.2:
|
||||||
resolution: {integrity: sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==}
|
resolution: {integrity: sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==}
|
||||||
dev: false
|
dev: false
|
||||||
|
@ -289,14 +276,25 @@ packages:
|
||||||
safe-buffer: 5.1.2
|
safe-buffer: 5.1.2
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
/tar-stream@3.0.0:
|
/tar-stream@3.1.2:
|
||||||
resolution: {integrity: sha512-O6OfUKBbQOqAhh6owTWmA730J/yZCYcpmZ1DBj2YX51ZQrt7d7NgzrR+CnO9wP6nt/viWZW2XeXLavX3/ZEbEg==}
|
resolution: {integrity: sha512-rEZHMKQop/sTykFtONCcOxyyLA+9hriHJlECxfh4z+Nfew87XGprDLp9RYFCd7yU+z3ePXfHlPbZrzgSLvc16A==}
|
||||||
dependencies:
|
dependencies:
|
||||||
b4a: 1.6.4
|
b4a: 1.6.4
|
||||||
bl: 6.0.2
|
|
||||||
streamx: 2.15.0
|
streamx: 2.15.0
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
|
/tar@6.1.15:
|
||||||
|
resolution: {integrity: sha512-/zKt9UyngnxIT/EAGYuxaMYgOIJiP81ab9ZfkILq4oNLPFX50qyYmu7jRj9qeXoxmJHjGlbH0+cm2uy1WCs10A==}
|
||||||
|
engines: {node: '>=10'}
|
||||||
|
dependencies:
|
||||||
|
chownr: 2.0.0
|
||||||
|
fs-minipass: 2.1.0
|
||||||
|
minipass: 5.0.0
|
||||||
|
minizlib: 2.1.2
|
||||||
|
mkdirp: 1.0.4
|
||||||
|
yallist: 4.0.0
|
||||||
|
dev: false
|
||||||
|
|
||||||
/through2@2.0.5:
|
/through2@2.0.5:
|
||||||
resolution: {integrity: sha512-/mrRod8xqpA+IHSLyGCQ2s8SPHiCDEeQJSep1jqLYeEUClOFG2Qsh+4FU6G9VeqpZnGW/Su8LQGc4YKni5rYSQ==}
|
resolution: {integrity: sha512-/mrRod8xqpA+IHSLyGCQ2s8SPHiCDEeQJSep1jqLYeEUClOFG2Qsh+4FU6G9VeqpZnGW/Su8LQGc4YKni5rYSQ==}
|
||||||
dependencies:
|
dependencies:
|
||||||
|
@ -315,14 +313,6 @@ 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
|
||||||
|
@ -331,3 +321,7 @@ packages:
|
||||||
resolution: {integrity: sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==}
|
resolution: {integrity: sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==}
|
||||||
engines: {node: '>=0.4'}
|
engines: {node: '>=0.4'}
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
|
/yallist@4.0.0:
|
||||||
|
resolution: {integrity: sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==}
|
||||||
|
dev: false
|
|
@ -2,13 +2,10 @@
|
||||||
// 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";
|
||||||
import { Duplex, Writable } from "node:stream";
|
import { Readable } from "node:stream";
|
||||||
import { ReadableStream } from "node:stream/web";
|
|
||||||
import { extract, pack } from "tar-stream";
|
import { extract, pack } from "tar-stream";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -26,11 +23,10 @@ import { extract, pack } from "tar-stream";
|
||||||
* squashfs image that can be mounted inside the VM. This way, we reuse downloaded images
|
* squashfs image that can be mounted inside the VM. This way, we reuse downloaded images
|
||||||
* efficiently.
|
* efficiently.
|
||||||
*
|
*
|
||||||
* @param {Promise<ReadableStream>[]} streams
|
* @param streams {Promise<Readable>[]}
|
||||||
* @param {string} output
|
* @param output {string}
|
||||||
* @param {{ content: string, headers: import("tar-stream").Headers }[]} extraFiles
|
|
||||||
*/
|
*/
|
||||||
export async function tar2squashfs(streams, output, extraFiles) {
|
export async function tar2squashfs(streams, output) {
|
||||||
const child = spawn(
|
const child = spawn(
|
||||||
"mksquashfs",
|
"mksquashfs",
|
||||||
[
|
[
|
||||||
|
@ -41,18 +37,14 @@ export async function tar2squashfs(streams, output, extraFiles) {
|
||||||
...["-Xcompression-level", "3"],
|
...["-Xcompression-level", "3"],
|
||||||
],
|
],
|
||||||
{
|
{
|
||||||
// stdio: "pipe",
|
|
||||||
stdio: ["pipe", "inherit", "inherit"],
|
stdio: ["pipe", "inherit", "inherit"],
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
const p = pack();
|
const p = pack();
|
||||||
p.pipe(child.stdin);
|
p.pipe(child.stdin);
|
||||||
p.on("error", console.error);
|
|
||||||
|
|
||||||
// We reverse the arrays because mksquashfs ignores files if they already exist,
|
for (const streamP of streams) {
|
||||||
// so we leave the last layers first so they are the ones used instead of the last ones
|
|
||||||
for (const streamP of [...streams].reverse()) {
|
|
||||||
const stream = await streamP;
|
const stream = await streamP;
|
||||||
const ex = extract();
|
const ex = extract();
|
||||||
|
|
||||||
|
@ -60,7 +52,7 @@ export async function tar2squashfs(streams, output, extraFiles) {
|
||||||
stream.pipe(p.entry(header, next));
|
stream.pipe(p.entry(header, next));
|
||||||
});
|
});
|
||||||
|
|
||||||
stream.pipeThrough(Duplex.toWeb(gunzip())).pipeTo(Writable.toWeb(ex));
|
stream.pipe(gunzip()).pipe(ex);
|
||||||
|
|
||||||
await new Promise((resolve) =>
|
await new Promise((resolve) =>
|
||||||
ex.on("finish", () => {
|
ex.on("finish", () => {
|
||||||
|
@ -68,11 +60,7 @@ export async function tar2squashfs(streams, output, extraFiles) {
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
console.debug("finalizing");
|
||||||
for (const { headers, content } of extraFiles) {
|
|
||||||
p.entry(headers, content);
|
|
||||||
}
|
|
||||||
|
|
||||||
p.finalize();
|
p.finalize();
|
||||||
|
|
||||||
await new Promise((resolve, reject) =>
|
await new Promise((resolve, reject) =>
|
|
@ -3,6 +3,9 @@
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"allowJs": true,
|
"allowJs": true,
|
||||||
"checkJs": true,
|
"checkJs": true,
|
||||||
|
"lib": ["es2023"],
|
||||||
|
"module": "Node16",
|
||||||
|
"target": "es2022",
|
||||||
"noEmit": true
|
"noEmit": true
|
||||||
}
|
}
|
||||||
}
|
}
|
203
main.go
Normal file
203
main.go
Normal file
|
@ -0,0 +1,203 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
// https://stanislas.blog/2021/08/firecracker/
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"io/ioutil"
|
||||||
|
"log"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/firecracker-microvm/firecracker-go-sdk"
|
||||||
|
"github.com/firecracker-microvm/firecracker-go-sdk/client/models"
|
||||||
|
"github.com/jaevor/go-nanoid"
|
||||||
|
"github.com/labstack/echo/v4"
|
||||||
|
"github.com/labstack/echo/v4/middleware"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
e := echo.New()
|
||||||
|
e.Use(middleware.Logger())
|
||||||
|
e.Use(middleware.Recover())
|
||||||
|
e.POST("/run", run)
|
||||||
|
|
||||||
|
e.Logger.Fatal(e.Start(":8080"))
|
||||||
|
}
|
||||||
|
|
||||||
|
type runResp struct {
|
||||||
|
VmId string
|
||||||
|
}
|
||||||
|
|
||||||
|
func run(c echo.Context) error {
|
||||||
|
script, err := ioutil.ReadAll(c.Request().Body)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
vmid, agent, m := startVM()
|
||||||
|
err = agent.run(script)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
ctx := context.Background()
|
||||||
|
if err := m.Wait(ctx); err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
defer agent.off()
|
||||||
|
|
||||||
|
return c.JSON(http.StatusOK, runResp{
|
||||||
|
VmId: vmid,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func startVM() (string, agentConfig, *firecracker.Machine) {
|
||||||
|
nanid, err := nanoid.Standard(21)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
vmid := nanid()
|
||||||
|
secret := nanid()
|
||||||
|
socketPath := "/tmp/firecracker-" + vmid + ".sock"
|
||||||
|
|
||||||
|
cfg := firecracker.Config{
|
||||||
|
SocketPath: socketPath,
|
||||||
|
KernelImagePath: "vmlinux.bin",
|
||||||
|
Drives: firecracker.NewDrivesBuilder("./rootfs.ext4").Build(),
|
||||||
|
NetworkInterfaces: []firecracker.NetworkInterface{{
|
||||||
|
CNIConfiguration: &firecracker.CNIConfiguration{
|
||||||
|
NetworkName: "fcnet",
|
||||||
|
IfName: "veth0-fire",
|
||||||
|
BinPath: []string{"/opt/cni/bin", "/usr/libexec/cni"},
|
||||||
|
},
|
||||||
|
}},
|
||||||
|
MachineCfg: models.MachineConfiguration{
|
||||||
|
VcpuCount: firecracker.Int64(1),
|
||||||
|
MemSizeMib: firecracker.Int64(1024),
|
||||||
|
},
|
||||||
|
// TODO: setup jailer
|
||||||
|
KernelArgs: "console=ttyS0 reboot=k panic=1 pci=off fireactions.secret=" + secret,
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: change stdout/stderr files
|
||||||
|
stdoutPath := "/tmp/stdout.log"
|
||||||
|
stdout, err := os.OpenFile(stdoutPath, os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0644)
|
||||||
|
if err != nil {
|
||||||
|
panic(fmt.Errorf("failed to create stdout file: %v", err))
|
||||||
|
}
|
||||||
|
stderrPath := "/tmp/stderr.log"
|
||||||
|
stderr, err := os.OpenFile(stderrPath, os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0644)
|
||||||
|
if err != nil {
|
||||||
|
panic(fmt.Errorf("failed to create stderr file: %v", err))
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx := context.Background()
|
||||||
|
// build our custom command that contains our two files to
|
||||||
|
// write to during process execution
|
||||||
|
cmd := firecracker.VMCommandBuilder{}.
|
||||||
|
WithBin("firecracker").
|
||||||
|
WithSocketPath(socketPath).
|
||||||
|
WithStdout(stdout).
|
||||||
|
WithStderr(stderr).
|
||||||
|
Build(ctx)
|
||||||
|
|
||||||
|
m, err := firecracker.NewMachine(ctx, cfg, firecracker.WithProcessRunner(cmd))
|
||||||
|
if err != nil {
|
||||||
|
panic(fmt.Errorf("failed to create new machine: %v", err))
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := m.Start(ctx); err != nil {
|
||||||
|
panic(fmt.Errorf("failed to initialize machine: %v", err))
|
||||||
|
}
|
||||||
|
|
||||||
|
ip := m.Cfg.NetworkInterfaces[0].StaticConfiguration.IPConfiguration.IPAddr.IP
|
||||||
|
log.Printf("IP: %s", ip.String())
|
||||||
|
|
||||||
|
// defer m.StopVMM()
|
||||||
|
|
||||||
|
agent := agentConfig{ip: ip.String(), secret: secret}
|
||||||
|
|
||||||
|
if err := agent.waitForAgent(); err != nil {
|
||||||
|
log.Panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
ctx := context.Background()
|
||||||
|
if err := m.Wait(ctx); err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
os.Remove(cfg.SocketPath)
|
||||||
|
}()
|
||||||
|
// if err := m.Wait(ctx); err != nil {
|
||||||
|
// panic(err)
|
||||||
|
// }
|
||||||
|
|
||||||
|
return vmid, agent, m
|
||||||
|
}
|
||||||
|
|
||||||
|
type agentConfig struct {
|
||||||
|
ip string
|
||||||
|
secret string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a agentConfig) request() *http.Request {
|
||||||
|
req, err := http.NewRequest("GET", "http://"+a.ip+":8080/hello", nil)
|
||||||
|
req.Header.Set("Authorization", "Bearer "+a.secret)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
return req
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a agentConfig) run(script []byte) error {
|
||||||
|
req, err := http.NewRequest("POST", "http://"+a.ip+":8080/run", bytes.NewBuffer(script))
|
||||||
|
req.Header.Set("Authorization", "Bearer "+a.secret)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
res, err := http.DefaultClient.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
byt, err := ioutil.ReadAll(res.Body)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
log.Println("[QIOdLqxLoKIMQ0uGoxNuu]", string(byt))
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a agentConfig) off() error {
|
||||||
|
return a.run([]byte("#!/bin/sh\nreboot"))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a agentConfig) waitForAgent() error {
|
||||||
|
client := http.Client{
|
||||||
|
Timeout: time.Millisecond * 50,
|
||||||
|
}
|
||||||
|
for {
|
||||||
|
log.Println("waiting for agent to come up...")
|
||||||
|
req := a.request()
|
||||||
|
req.Method = "GET"
|
||||||
|
req.URL.Path = "/hello"
|
||||||
|
resp, err := client.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
time.Sleep(time.Millisecond * 200)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if resp.StatusCode != http.StatusOK {
|
||||||
|
log.Println("Agent is failing", resp)
|
||||||
|
return errors.New("Agent is failing")
|
||||||
|
}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
31
readme.md
31
readme.md
|
@ -1,29 +1,18 @@
|
||||||
# fireactions
|
|
||||||
|
|
||||||
¡WIP!
|
|
||||||
|
|
||||||
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/>
|
|
||||||
|
|
||||||
## setup
|
## setup
|
||||||
|
|
||||||
|
```sh
|
||||||
|
sudo env GOBIN=/opt/cni/bin go install github.com/awslabs/tc-redirect-tap/cmd/tc-redirect-tap@latest
|
||||||
|
sudo dnf install containernetworking-plugins
|
||||||
|
```
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
# build rootfs image (incl. agent)
|
# build rootfs image (incl. agent)
|
||||||
(cd rootfs; pnpm install)
|
(cd rootfs; pnpm install)
|
||||||
node rootfs/index.js
|
node rootfs/index.js
|
||||||
# run daemon
|
# build & run daemon
|
||||||
(cd server; pnpm install && pnpm start) &
|
go build && sudo ./fireactions
|
||||||
# run VM
|
# run VM
|
||||||
curl "localhost:8080/run?image=gitea.nulo.in/nulo/super-image:3.18"
|
curl --data-raw "#!/bin/sh"\n"echo hi" localhost:8080/run
|
||||||
|
# inspect VM tty
|
||||||
|
tail -f /tmp/std*
|
||||||
```
|
```
|
||||||
|
|
||||||
## componentes
|
|
||||||
|
|
||||||
1. `server` es el servidor de api (JavaScript/TypeScript) que recibe pedidos, descarga imagenes OCI y inicia firecracker con los parametros apropiados.
|
|
||||||
1. `rootfs` es el código (JavaScript/TypeScript) que genera el rootfs con todo lo necesario para configurar la vm (red (WIP), mounts básicos, etc) y montar la imágen.
|
|
||||||
1. `rootfs/init` es un programa (Rust) que hace las cosas básicas antes de y para montar la imágen OCI y hace chroot.
|
|
||||||
|
|
||||||
|
|
145
rootfs/build.js
145
rootfs/build.js
|
@ -1,15 +1,13 @@
|
||||||
// @ts-check
|
|
||||||
import { init } from "@nulo/apkit";
|
import { init } from "@nulo/apkit";
|
||||||
import { execFile as _execFile } from "node:child_process";
|
import { execFile as _execFile } from "node:child_process";
|
||||||
import {
|
import {
|
||||||
chmod,
|
chmod,
|
||||||
copyFile,
|
|
||||||
lstat,
|
|
||||||
mkdir,
|
mkdir,
|
||||||
mkdtemp,
|
mkdtemp,
|
||||||
readFile,
|
readFile,
|
||||||
readdir,
|
readdir,
|
||||||
rm,
|
rm,
|
||||||
|
symlink,
|
||||||
writeFile,
|
writeFile,
|
||||||
} from "node:fs/promises";
|
} from "node:fs/promises";
|
||||||
import { tmpdir } from "node:os";
|
import { tmpdir } from "node:os";
|
||||||
|
@ -19,70 +17,73 @@ const execFile = promisify(_execFile);
|
||||||
|
|
||||||
const root = await mkdtemp(join(tmpdir(), "fireactions-rootfs."));
|
const root = await mkdtemp(join(tmpdir(), "fireactions-rootfs."));
|
||||||
const alpine = await init(root);
|
const alpine = await init(root);
|
||||||
await alpine.install(["util-linux", "dhcpcd", "eudev"]);
|
await alpine.install(["dropbear", "util-linux", "dropbear-dbclient", "dhcpcd"]);
|
||||||
|
|
||||||
const initPath = await buildInit();
|
// gotta go fast
|
||||||
await copyFile(initPath, r("/sbin/fireactions-init"));
|
await append(r("/etc/rc.conf"), 'rc_parallel="YES"');
|
||||||
|
|
||||||
await writeSbin(
|
await mkdirp(r("/usr/local/sbin"));
|
||||||
r("/sbin/init"),
|
console.debug(
|
||||||
`#!/bin/sh
|
await execFile("go", [
|
||||||
/etc/init-files/00-pseudofs.sh
|
"build",
|
||||||
/etc/init-files/05-misc.sh
|
"-tags=netgo",
|
||||||
|
"-o",
|
||||||
#exec /sbin/fireactions-init
|
r("/usr/local/sbin/fireactions-agent"),
|
||||||
/sbin/fireactions-init || sh
|
"./agent",
|
||||||
|
])
|
||||||
|
);
|
||||||
|
// https://github.com/OpenRC/openrc/blob/master/service-script-guide.md
|
||||||
|
await writeFile(
|
||||||
|
r("/etc/init.d/fireactions-agent"),
|
||||||
|
`#!/sbin/openrc-run
|
||||||
|
pidfile="/run/\${RC_SVCNAME}.pid"
|
||||||
|
command_background=true
|
||||||
|
command=/usr/local/sbin/fireactions-agent
|
||||||
|
output_log=/dev/stdout
|
||||||
|
error_log=/dev/stdout
|
||||||
`
|
`
|
||||||
);
|
);
|
||||||
|
await chmod(r("/etc/init.d/fireactions-agent"), 0o700);
|
||||||
|
await alpine.rcUpdate("default", "fireactions-agent");
|
||||||
|
|
||||||
await mkdir(r("/etc/init-files/"));
|
await alpine.rcUpdate("boot", "devfs");
|
||||||
await writeSbin(
|
await alpine.rcUpdate("boot", "procfs");
|
||||||
r("/etc/init-files/00-pseudofs.sh"),
|
await alpine.rcUpdate("boot", "sysfs");
|
||||||
`#!/bin/sh
|
// alpine.rcUpdate('default', 'dhcpcd')
|
||||||
mountpoint -q /proc || mount -o nosuid,noexec,nodev -t proc proc /proc
|
await alpine.rcUpdate("default", "dropbear");
|
||||||
mountpoint -q /sys || mount -o nosuid,noexec,nodev -t sysfs sys /sys
|
|
||||||
mountpoint -q /run || mount -o mode=0755,nosuid,nodev -t tmpfs run /run
|
await writeFile(r("/etc/securetty"), "ttyS0");
|
||||||
mountpoint -q /dev || mount -o mode=0755,nosuid -t devtmpfs dev /dev
|
await symlink("agetty", r("/etc/init.d/agetty.ttyS0"));
|
||||||
mkdir -p -m0755 /run/runit /run/lvm /run/user /run/lock /run/log /dev/pts /dev/shm
|
await alpine.rcUpdate("default", "agetty.ttyS0");
|
||||||
mountpoint -q /dev/pts || mount -o mode=0620,gid=5,nosuid,noexec -n -t devpts devpts /dev/pts
|
|
||||||
mountpoint -q /dev/shm || mount -o mode=1777,nosuid,nodev -n -t tmpfs shm /dev/shm
|
await mkdirp(r("/etc/dropbear"));
|
||||||
mountpoint -q /sys/kernel/security || mount -n -t securityfs securityfs /sys/kernel/security
|
for (const t of ["rsa", "dss", "ed25519", "ecdsa"]) {
|
||||||
`
|
await execFile("dropbearkey", [
|
||||||
|
"-t",
|
||||||
|
t,
|
||||||
|
"-f",
|
||||||
|
r(`/etc/dropbear/dropbear_${t}_host_key`),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
await execFile("sudo", ["mkdir", "-p", r("/root/.ssh")]);
|
||||||
|
await writeFile(
|
||||||
|
"authorized_keys",
|
||||||
|
"ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIEhrcTPMrhrOdwpgFkRFjTt8rdxt3gg16LJvBCZENRWa user@personal"
|
||||||
|
);
|
||||||
|
await execFile("sudo", [
|
||||||
|
"mv",
|
||||||
|
"authorized_keys",
|
||||||
|
r("/root/.ssh/authorized_keys"),
|
||||||
|
]);
|
||||||
|
|
||||||
|
await alpine.rcUpdate("default", "networking");
|
||||||
|
await writeFile(
|
||||||
|
r("/etc/network/interfaces"),
|
||||||
|
`auto lo
|
||||||
|
iface lo inet loopback`
|
||||||
);
|
);
|
||||||
|
|
||||||
await writeSbin(
|
|
||||||
r("/etc/init-files/05-misc.sh"),
|
|
||||||
`#!/bin/sh
|
|
||||||
install -m0664 -o root -g utmp /dev/null /run/utmp
|
|
||||||
#halt -B # for wtmp
|
|
||||||
|
|
||||||
ip link set up dev lo
|
|
||||||
hostname -F /etc/hostname
|
|
||||||
`
|
|
||||||
);
|
|
||||||
|
|
||||||
// await mkdirp(r("/etc/dropbear"));
|
|
||||||
// for (const t of ["rsa", "dss", "ed25519", "ecdsa"]) {
|
|
||||||
// await execFile("dropbearkey", [
|
|
||||||
// "-t",
|
|
||||||
// t,
|
|
||||||
// "-f",
|
|
||||||
// r(`/etc/dropbear/dropbear_${t}_host_key`),
|
|
||||||
// ]);
|
|
||||||
// }
|
|
||||||
|
|
||||||
// await execFile("sudo", ["mkdir", "-p", r("/root/.ssh")]);
|
|
||||||
// await writeFile(
|
|
||||||
// "authorized_keys",
|
|
||||||
// "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIEhrcTPMrhrOdwpgFkRFjTt8rdxt3gg16LJvBCZENRWa user@personal"
|
|
||||||
// );
|
|
||||||
// await execFile("sudo", [
|
|
||||||
// "mv",
|
|
||||||
// "authorized_keys",
|
|
||||||
// r("/root/.ssh/authorized_keys"),
|
|
||||||
// ]);
|
|
||||||
|
|
||||||
// TODO: hacer squashfs
|
|
||||||
const ext4 = "rootfs.ext4";
|
const ext4 = "rootfs.ext4";
|
||||||
await rm(ext4);
|
await rm(ext4);
|
||||||
await execFile("fallocate", ["--length", "1G", ext4]);
|
await execFile("fallocate", ["--length", "1G", ext4]);
|
||||||
|
@ -128,27 +129,3 @@ async function mkdirp(path) {
|
||||||
function r(path) {
|
function r(path) {
|
||||||
return join(root, path);
|
return join(root, path);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* @param {import("fs").PathLike} path
|
|
||||||
* @param {string} content
|
|
||||||
*/
|
|
||||||
async function writeSbin(path, content) {
|
|
||||||
try {
|
|
||||||
await lstat(path);
|
|
||||||
await rm(path, { recursive: true });
|
|
||||||
} catch {}
|
|
||||||
await writeFile(path, content);
|
|
||||||
await chmod(path, 0o500);
|
|
||||||
}
|
|
||||||
|
|
||||||
async function buildInit() {
|
|
||||||
const srcPath = "./rootfs/init";
|
|
||||||
await execFile("podman", [
|
|
||||||
...["run", "--rm", "-it"],
|
|
||||||
...["-v", `${srcPath}:/home/rust/src:Z`],
|
|
||||||
"docker.io/messense/rust-musl-cross:x86_64-musl",
|
|
||||||
...["cargo", "build", "--release"],
|
|
||||||
]);
|
|
||||||
return join(srcPath, "target/x86_64-unknown-linux-musl/release/init");
|
|
||||||
}
|
|
||||||
|
|
1
rootfs/init/.gitignore
vendored
1
rootfs/init/.gitignore
vendored
|
@ -1 +0,0 @@
|
||||||
target/
|
|
565
rootfs/init/Cargo.lock
generated
565
rootfs/init/Cargo.lock
generated
|
@ -1,565 +0,0 @@
|
||||||
# This file is automatically @generated by Cargo.
|
|
||||||
# It is not intended for manual editing.
|
|
||||||
version = 3
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "anyhow"
|
|
||||||
version = "1.0.71"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "9c7d0618f0e0b7e8ff11427422b64564d5fb0be1940354bfe2e0529b18a9d9b8"
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "autocfg"
|
|
||||||
version = "1.1.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa"
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "bitflags"
|
|
||||||
version = "1.3.2"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "byteorder"
|
|
||||||
version = "1.4.3"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "14c189c53d098945499cdfa7ecc63567cf3886b3332b312a5b4585d8d3a6a610"
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "bytes"
|
|
||||||
version = "1.4.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "89b2fd2a0dcf38d7971e2194b6b6eebab45ae01067456a7fd93d5547a61b70be"
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "cc"
|
|
||||||
version = "1.0.79"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "50d30906286121d95be3d479533b458f87493b30a4b5f79a607db8f5d11aa91f"
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "cfg-if"
|
|
||||||
version = "1.0.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "futures"
|
|
||||||
version = "0.3.28"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "23342abe12aba583913b2e62f22225ff9c950774065e4bfb61a19cd9770fec40"
|
|
||||||
dependencies = [
|
|
||||||
"futures-channel",
|
|
||||||
"futures-core",
|
|
||||||
"futures-executor",
|
|
||||||
"futures-io",
|
|
||||||
"futures-sink",
|
|
||||||
"futures-task",
|
|
||||||
"futures-util",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "futures-channel"
|
|
||||||
version = "0.3.28"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "955518d47e09b25bbebc7a18df10b81f0c766eaf4c4f1cccef2fca5f2a4fb5f2"
|
|
||||||
dependencies = [
|
|
||||||
"futures-core",
|
|
||||||
"futures-sink",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "futures-core"
|
|
||||||
version = "0.3.28"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "4bca583b7e26f571124fe5b7561d49cb2868d79116cfa0eefce955557c6fee8c"
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "futures-executor"
|
|
||||||
version = "0.3.28"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "ccecee823288125bd88b4d7f565c9e58e41858e47ab72e8ea2d64e93624386e0"
|
|
||||||
dependencies = [
|
|
||||||
"futures-core",
|
|
||||||
"futures-task",
|
|
||||||
"futures-util",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "futures-io"
|
|
||||||
version = "0.3.28"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "4fff74096e71ed47f8e023204cfd0aa1289cd54ae5430a9523be060cdb849964"
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "futures-macro"
|
|
||||||
version = "0.3.28"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "89ca545a94061b6365f2c7355b4b32bd20df3ff95f02da9329b34ccc3bd6ee72"
|
|
||||||
dependencies = [
|
|
||||||
"proc-macro2",
|
|
||||||
"quote",
|
|
||||||
"syn",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "futures-sink"
|
|
||||||
version = "0.3.28"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "f43be4fe21a13b9781a69afa4985b0f6ee0e1afab2c6f454a8cf30e2b2237b6e"
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "futures-task"
|
|
||||||
version = "0.3.28"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "76d3d132be6c0e6aa1534069c705a74a5997a356c0dc2f86a47765e5617c5b65"
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "futures-util"
|
|
||||||
version = "0.3.28"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "26b01e40b772d54cf6c6d721c1d1abd0647a0106a12ecaa1c186273392a69533"
|
|
||||||
dependencies = [
|
|
||||||
"futures-channel",
|
|
||||||
"futures-core",
|
|
||||||
"futures-io",
|
|
||||||
"futures-macro",
|
|
||||||
"futures-sink",
|
|
||||||
"futures-task",
|
|
||||||
"memchr",
|
|
||||||
"pin-project-lite",
|
|
||||||
"pin-utils",
|
|
||||||
"slab",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "hermit-abi"
|
|
||||||
version = "0.2.6"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "ee512640fe35acbfb4bb779db6f0d80704c2cacfa2e39b601ef3e3f47d1ae4c7"
|
|
||||||
dependencies = [
|
|
||||||
"libc",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "init"
|
|
||||||
version = "0.1.0"
|
|
||||||
dependencies = [
|
|
||||||
"futures",
|
|
||||||
"nix 0.19.1",
|
|
||||||
"rtnetlink",
|
|
||||||
"serde",
|
|
||||||
"serde_json",
|
|
||||||
"tokio",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "itoa"
|
|
||||||
version = "1.0.6"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "453ad9f582a441959e5f0d088b02ce04cfe8d51a8eaf077f12ac6d3e94164ca6"
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "libc"
|
|
||||||
version = "0.2.146"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "f92be4933c13fd498862a9e02a3055f8a8d9c039ce33db97306fd5a6caa7f29b"
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "log"
|
|
||||||
version = "0.4.19"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "b06a4cde4c0f271a446782e3eff8de789548ce57dbc8eca9292c27f4a42004b4"
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "memchr"
|
|
||||||
version = "2.5.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "2dffe52ecf27772e601905b7522cb4ef790d2cc203488bbd0e2fe85fcb74566d"
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "mio"
|
|
||||||
version = "0.8.8"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "927a765cd3fc26206e66b296465fa9d3e5ab003e651c1b3c060e7956d96b19d2"
|
|
||||||
dependencies = [
|
|
||||||
"libc",
|
|
||||||
"wasi",
|
|
||||||
"windows-sys",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "netlink-packet-core"
|
|
||||||
version = "0.5.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "7e5cf0b54effda4b91615c40ff0fd12d0d4c9a6e0f5116874f03941792ff535a"
|
|
||||||
dependencies = [
|
|
||||||
"anyhow",
|
|
||||||
"byteorder",
|
|
||||||
"libc",
|
|
||||||
"netlink-packet-utils",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "netlink-packet-route"
|
|
||||||
version = "0.15.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "ea993e32c77d87f01236c38f572ecb6c311d592e56a06262a007fd2a6e31253c"
|
|
||||||
dependencies = [
|
|
||||||
"anyhow",
|
|
||||||
"bitflags",
|
|
||||||
"byteorder",
|
|
||||||
"libc",
|
|
||||||
"netlink-packet-core",
|
|
||||||
"netlink-packet-utils",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "netlink-packet-utils"
|
|
||||||
version = "0.5.2"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "0ede8a08c71ad5a95cdd0e4e52facd37190977039a4704eb82a283f713747d34"
|
|
||||||
dependencies = [
|
|
||||||
"anyhow",
|
|
||||||
"byteorder",
|
|
||||||
"paste",
|
|
||||||
"thiserror",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "netlink-proto"
|
|
||||||
version = "0.11.1"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "26305d12193227ef7b8227e7d61ae4eaf174607f79bd8eeceff07aacaefde497"
|
|
||||||
dependencies = [
|
|
||||||
"bytes",
|
|
||||||
"futures",
|
|
||||||
"log",
|
|
||||||
"netlink-packet-core",
|
|
||||||
"netlink-sys",
|
|
||||||
"thiserror",
|
|
||||||
"tokio",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "netlink-sys"
|
|
||||||
version = "0.8.5"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "6471bf08e7ac0135876a9581bf3217ef0333c191c128d34878079f42ee150411"
|
|
||||||
dependencies = [
|
|
||||||
"bytes",
|
|
||||||
"futures",
|
|
||||||
"libc",
|
|
||||||
"log",
|
|
||||||
"tokio",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "nix"
|
|
||||||
version = "0.19.1"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "b2ccba0cfe4fdf15982d1674c69b1fd80bad427d293849982668dfe454bd61f2"
|
|
||||||
dependencies = [
|
|
||||||
"bitflags",
|
|
||||||
"cc",
|
|
||||||
"cfg-if",
|
|
||||||
"libc",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "nix"
|
|
||||||
version = "0.26.2"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "bfdda3d196821d6af13126e40375cdf7da646a96114af134d5f417a9a1dc8e1a"
|
|
||||||
dependencies = [
|
|
||||||
"bitflags",
|
|
||||||
"cfg-if",
|
|
||||||
"libc",
|
|
||||||
"static_assertions",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "num_cpus"
|
|
||||||
version = "1.15.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "0fac9e2da13b5eb447a6ce3d392f23a29d8694bff781bf03a16cd9ac8697593b"
|
|
||||||
dependencies = [
|
|
||||||
"hermit-abi",
|
|
||||||
"libc",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "paste"
|
|
||||||
version = "1.0.12"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "9f746c4065a8fa3fe23974dd82f15431cc8d40779821001404d10d2e79ca7d79"
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "pin-project-lite"
|
|
||||||
version = "0.2.9"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "e0a7ae3ac2f1173085d398531c705756c94a4c56843785df85a60c1a0afac116"
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "pin-utils"
|
|
||||||
version = "0.1.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184"
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "proc-macro2"
|
|
||||||
version = "1.0.60"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "dec2b086b7a862cf4de201096214fa870344cf922b2b30c167badb3af3195406"
|
|
||||||
dependencies = [
|
|
||||||
"unicode-ident",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "quote"
|
|
||||||
version = "1.0.28"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "1b9ab9c7eadfd8df19006f1cf1a4aed13540ed5cbc047010ece5826e10825488"
|
|
||||||
dependencies = [
|
|
||||||
"proc-macro2",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "rtnetlink"
|
|
||||||
version = "0.12.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "ed7d42da676fdf7e470e2502717587dd1089d8b48d9d1b846dcc3c01072858cb"
|
|
||||||
dependencies = [
|
|
||||||
"futures",
|
|
||||||
"log",
|
|
||||||
"netlink-packet-core",
|
|
||||||
"netlink-packet-route",
|
|
||||||
"netlink-packet-utils",
|
|
||||||
"netlink-proto",
|
|
||||||
"netlink-sys",
|
|
||||||
"nix 0.26.2",
|
|
||||||
"thiserror",
|
|
||||||
"tokio",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "ryu"
|
|
||||||
version = "1.0.13"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "f91339c0467de62360649f8d3e185ca8de4224ff281f66000de5eb2a77a79041"
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "serde"
|
|
||||||
version = "1.0.164"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "9e8c8cf938e98f769bc164923b06dce91cea1751522f46f8466461af04c9027d"
|
|
||||||
dependencies = [
|
|
||||||
"serde_derive",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "serde_derive"
|
|
||||||
version = "1.0.164"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "d9735b638ccc51c28bf6914d90a2e9725b377144fc612c49a611fddd1b631d68"
|
|
||||||
dependencies = [
|
|
||||||
"proc-macro2",
|
|
||||||
"quote",
|
|
||||||
"syn",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "serde_json"
|
|
||||||
version = "1.0.97"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "bdf3bf93142acad5821c99197022e170842cdbc1c30482b98750c688c640842a"
|
|
||||||
dependencies = [
|
|
||||||
"itoa",
|
|
||||||
"ryu",
|
|
||||||
"serde",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "slab"
|
|
||||||
version = "0.4.8"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "6528351c9bc8ab22353f9d776db39a20288e8d6c37ef8cfe3317cf875eecfc2d"
|
|
||||||
dependencies = [
|
|
||||||
"autocfg",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "socket2"
|
|
||||||
version = "0.4.9"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "64a4a911eed85daf18834cfaa86a79b7d266ff93ff5ba14005426219480ed662"
|
|
||||||
dependencies = [
|
|
||||||
"libc",
|
|
||||||
"winapi",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "static_assertions"
|
|
||||||
version = "1.1.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f"
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "syn"
|
|
||||||
version = "2.0.18"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "32d41677bcbe24c20c52e7c70b0d8db04134c5d1066bf98662e2871ad200ea3e"
|
|
||||||
dependencies = [
|
|
||||||
"proc-macro2",
|
|
||||||
"quote",
|
|
||||||
"unicode-ident",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "thiserror"
|
|
||||||
version = "1.0.40"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "978c9a314bd8dc99be594bc3c175faaa9794be04a5a5e153caba6915336cebac"
|
|
||||||
dependencies = [
|
|
||||||
"thiserror-impl",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "thiserror-impl"
|
|
||||||
version = "1.0.40"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "f9456a42c5b0d803c8cd86e73dd7cc9edd429499f37a3550d286d5e86720569f"
|
|
||||||
dependencies = [
|
|
||||||
"proc-macro2",
|
|
||||||
"quote",
|
|
||||||
"syn",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "tokio"
|
|
||||||
version = "1.28.2"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "94d7b1cfd2aa4011f2de74c2c4c63665e27a71006b0a192dcd2710272e73dfa2"
|
|
||||||
dependencies = [
|
|
||||||
"autocfg",
|
|
||||||
"bytes",
|
|
||||||
"libc",
|
|
||||||
"mio",
|
|
||||||
"num_cpus",
|
|
||||||
"pin-project-lite",
|
|
||||||
"socket2",
|
|
||||||
"tokio-macros",
|
|
||||||
"windows-sys",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "tokio-macros"
|
|
||||||
version = "2.1.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "630bdcf245f78637c13ec01ffae6187cca34625e8c63150d424b59e55af2675e"
|
|
||||||
dependencies = [
|
|
||||||
"proc-macro2",
|
|
||||||
"quote",
|
|
||||||
"syn",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "unicode-ident"
|
|
||||||
version = "1.0.9"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "b15811caf2415fb889178633e7724bad2509101cde276048e013b9def5e51fa0"
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "wasi"
|
|
||||||
version = "0.11.0+wasi-snapshot-preview1"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423"
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "winapi"
|
|
||||||
version = "0.3.9"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419"
|
|
||||||
dependencies = [
|
|
||||||
"winapi-i686-pc-windows-gnu",
|
|
||||||
"winapi-x86_64-pc-windows-gnu",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "winapi-i686-pc-windows-gnu"
|
|
||||||
version = "0.4.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "winapi-x86_64-pc-windows-gnu"
|
|
||||||
version = "0.4.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "windows-sys"
|
|
||||||
version = "0.48.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9"
|
|
||||||
dependencies = [
|
|
||||||
"windows-targets",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "windows-targets"
|
|
||||||
version = "0.48.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "7b1eb6f0cd7c80c79759c929114ef071b87354ce476d9d94271031c0497adfd5"
|
|
||||||
dependencies = [
|
|
||||||
"windows_aarch64_gnullvm",
|
|
||||||
"windows_aarch64_msvc",
|
|
||||||
"windows_i686_gnu",
|
|
||||||
"windows_i686_msvc",
|
|
||||||
"windows_x86_64_gnu",
|
|
||||||
"windows_x86_64_gnullvm",
|
|
||||||
"windows_x86_64_msvc",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "windows_aarch64_gnullvm"
|
|
||||||
version = "0.48.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "91ae572e1b79dba883e0d315474df7305d12f569b400fcf90581b06062f7e1bc"
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "windows_aarch64_msvc"
|
|
||||||
version = "0.48.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "b2ef27e0d7bdfcfc7b868b317c1d32c641a6fe4629c171b8928c7b08d98d7cf3"
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "windows_i686_gnu"
|
|
||||||
version = "0.48.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "622a1962a7db830d6fd0a69683c80a18fda201879f0f447f065a3b7467daa241"
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "windows_i686_msvc"
|
|
||||||
version = "0.48.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "4542c6e364ce21bf45d69fdd2a8e455fa38d316158cfd43b3ac1c5b1b19f8e00"
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "windows_x86_64_gnu"
|
|
||||||
version = "0.48.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "ca2b8a661f7628cbd23440e50b05d705db3686f894fc9580820623656af974b1"
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "windows_x86_64_gnullvm"
|
|
||||||
version = "0.48.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "7896dbc1f41e08872e9d5e8f8baa8fdd2677f29468c4e156210174edc7f7b953"
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "windows_x86_64_msvc"
|
|
||||||
version = "0.48.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "1a515f5799fe4961cb532f983ce2b23082366b898e52ffbce459c86f67c8378a"
|
|
|
@ -1,15 +0,0 @@
|
||||||
[package]
|
|
||||||
name = "init"
|
|
||||||
version = "0.1.0"
|
|
||||||
edition = "2021"
|
|
||||||
|
|
||||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
|
||||||
|
|
||||||
[dependencies]
|
|
||||||
serde = { version = "1.0.164", features = ["derive"] }
|
|
||||||
serde_json = "1.0.97"
|
|
||||||
nix = "0.19"
|
|
||||||
rtnetlink = { version = "0.12.0" }
|
|
||||||
tokio = { version = "1.0", features = ["macros", "rt", "rt-multi-thread"] }
|
|
||||||
futures = "0.3.28"
|
|
||||||
|
|
|
@ -1,210 +0,0 @@
|
||||||
use futures::TryStreamExt;
|
|
||||||
use nix::mount::MsFlags;
|
|
||||||
use serde::{Deserialize, Serialize};
|
|
||||||
use std::fs;
|
|
||||||
use std::net::Ipv4Addr;
|
|
||||||
use std::{env, io, io::Write, iter::Iterator, os::unix, path::Path, process};
|
|
||||||
// inspirado en https://github.com/superfly/init-snapshot/blob/public/src/bin/init/main.rs#L230
|
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize, Debug)]
|
|
||||||
#[serde(rename_all = "PascalCase")]
|
|
||||||
struct Config {
|
|
||||||
env: Vec<String>,
|
|
||||||
cmd: Option<Vec<String>>,
|
|
||||||
entrypoint: Option<Vec<String>>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize, Debug)]
|
|
||||||
struct ImageConfig {
|
|
||||||
architecture: String,
|
|
||||||
config: Config,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tokio::main]
|
|
||||||
async fn main() -> Result<(), io::Error> {
|
|
||||||
let new_root = mount_new_root()?;
|
|
||||||
// TODO: move to after chroot after we start mounting /proc (uses /proc/cmdline)
|
|
||||||
setup_net_interface()
|
|
||||||
.await
|
|
||||||
.expect("couldn't set up network interace");
|
|
||||||
let config_file = Path::new(&new_root).join(".fireactions-image-config.json");
|
|
||||||
let config_text = fs::read(config_file)?;
|
|
||||||
let config: ImageConfig = serde_json::from_slice(&config_text).unwrap();
|
|
||||||
println!("{:#?}", config);
|
|
||||||
|
|
||||||
unix::fs::chroot(new_root)?;
|
|
||||||
env::set_current_dir("/")?;
|
|
||||||
|
|
||||||
// https://github.com/opencontainers/image-spec/blob/b5ec432b1c946c09e1568b18ef70b654a93739f6/conversion.md#verbatim-fields
|
|
||||||
let args = match (config.config.cmd, config.config.entrypoint) {
|
|
||||||
(None, Some(args)) => args,
|
|
||||||
(Some(args), None) => args,
|
|
||||||
(None, None) => panic!("Invalid config"),
|
|
||||||
(Some(cmd), Some(entrypoint)) => {
|
|
||||||
let mut args = entrypoint.clone();
|
|
||||||
args.append(&mut cmd.clone());
|
|
||||||
args
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// TODO: setup hostname
|
|
||||||
// TODO: setup /etc/hosts
|
|
||||||
populate_resolvconf()?;
|
|
||||||
|
|
||||||
let mut child = process::Command::new(&args[0])
|
|
||||||
.args(&args[1..])
|
|
||||||
.env_clear()
|
|
||||||
.envs(config.config.env.iter().map(|s| s.split_once('=').unwrap()))
|
|
||||||
.spawn()?;
|
|
||||||
let status = child.wait()?;
|
|
||||||
if !status.success() {
|
|
||||||
println!("{}", status);
|
|
||||||
}
|
|
||||||
nix::sys::reboot::reboot(nix::sys::reboot::RebootMode::RB_AUTOBOOT)
|
|
||||||
.map(|_| {})
|
|
||||||
.unwrap();
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
// async fn get_link(handle: &rtnetlink::Handle, name: &'static str) -> _ {
|
|
||||||
// handle
|
|
||||||
// .link()
|
|
||||||
// .get()
|
|
||||||
// .match_name(name.to_string())
|
|
||||||
// .execute()
|
|
||||||
// .try_next()
|
|
||||||
// .await
|
|
||||||
// .expect("no lo link found")
|
|
||||||
// }
|
|
||||||
|
|
||||||
async fn setup_net_interface() -> Result<(), rtnetlink::Error> {
|
|
||||||
let (connection, handle, _) = rtnetlink::new_connection().unwrap();
|
|
||||||
tokio::spawn(connection);
|
|
||||||
|
|
||||||
println!("netlink: getting lo link");
|
|
||||||
let lo = handle
|
|
||||||
.link()
|
|
||||||
.get()
|
|
||||||
.match_name("lo".to_string())
|
|
||||||
.execute()
|
|
||||||
.try_next()
|
|
||||||
.await?
|
|
||||||
.expect("no lo link found");
|
|
||||||
|
|
||||||
println!("netlink: setting lo link \"up\"");
|
|
||||||
handle.link().set(lo.header.index).up().execute().await?;
|
|
||||||
|
|
||||||
let cmdline = fs::read("/proc/cmdline").expect("couldn't read /proc/cmdline");
|
|
||||||
let (guest_addr, host_addr) = cmdline
|
|
||||||
.split(|c| *c == b' ')
|
|
||||||
.map(|keyval| std::str::from_utf8(keyval).unwrap().split_once("="))
|
|
||||||
.filter_map(|o| o)
|
|
||||||
.find(|(key, _)| *key == "fireactions_ip")
|
|
||||||
.expect("didn't find fireactions_ip cmdline")
|
|
||||||
.1
|
|
||||||
.split_once(":")
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
println!("netlink: getting eth0 link");
|
|
||||||
let eth0 = handle
|
|
||||||
.link()
|
|
||||||
.get()
|
|
||||||
.match_name("eth0".to_string())
|
|
||||||
.execute()
|
|
||||||
.try_next()
|
|
||||||
.await?
|
|
||||||
.expect("no eth0 link found");
|
|
||||||
|
|
||||||
println!("netlink: setting eth0 link \"up\"");
|
|
||||||
handle
|
|
||||||
.link()
|
|
||||||
.set(eth0.header.index)
|
|
||||||
.up()
|
|
||||||
.mtu(1420)
|
|
||||||
.execute()
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
// XXX: la impl en init-snapshot es sketchy. pareciera que lo único que hace
|
|
||||||
// es desactivar el chequeo de checksum de paquetes, osea una mejora de perf.
|
|
||||||
// let _ = ethtool_set("eth0", ETHTOOL_SRXCSUM, 0);
|
|
||||||
// ethtool_set("eth0", ETHTOOL_STXCSUM, 0)?;
|
|
||||||
|
|
||||||
println!("netlink: adding ip");
|
|
||||||
let address = handle.address();
|
|
||||||
address
|
|
||||||
.add(eth0.header.index, guest_addr.parse().unwrap(), 24)
|
|
||||||
.execute()
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
println!("netlink: adding route");
|
|
||||||
let route = handle.route();
|
|
||||||
route
|
|
||||||
.add()
|
|
||||||
.v4()
|
|
||||||
.gateway(host_addr.parse().unwrap())
|
|
||||||
.execute()
|
|
||||||
.await?;
|
|
||||||
// TODO: ipv6
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn populate_resolvconf() -> Result<(), io::Error> {
|
|
||||||
let mut file = fs::OpenOptions::new()
|
|
||||||
.create(true)
|
|
||||||
.write(true)
|
|
||||||
.truncate(true)
|
|
||||||
.open("/etc/resolv.conf")?;
|
|
||||||
// XXX: usar otro DNS?
|
|
||||||
write!(&mut file, "nameserver\t{}\n", "1.1.1.1")
|
|
||||||
}
|
|
||||||
|
|
||||||
fn mount_new_root() -> Result<&'static str, io::Error> {
|
|
||||||
// process::Command::new("modprobe")
|
|
||||||
// .args(&["overlay"])
|
|
||||||
// .spawn()?
|
|
||||||
// .wait()?;
|
|
||||||
|
|
||||||
let image_root = "/image_src";
|
|
||||||
fs::create_dir_all(image_root)?;
|
|
||||||
nix::mount::mount::<_, _, _, [u8]>(
|
|
||||||
Some("/dev/vdb"),
|
|
||||||
image_root,
|
|
||||||
Some("squashfs"),
|
|
||||||
MsFlags::MS_RDONLY,
|
|
||||||
None,
|
|
||||||
)
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
let tmp_rw = "/tmp_rw";
|
|
||||||
fs::create_dir_all(tmp_rw)?;
|
|
||||||
nix::mount::mount::<_, _, _, [u8]>(
|
|
||||||
Some("overlaytemp"),
|
|
||||||
tmp_rw,
|
|
||||||
Some("tmpfs"),
|
|
||||||
MsFlags::empty(),
|
|
||||||
None,
|
|
||||||
)
|
|
||||||
.unwrap();
|
|
||||||
let tmp_upper = "/tmp_rw/upper";
|
|
||||||
fs::create_dir_all(tmp_upper)?;
|
|
||||||
let tmp_work = "/tmp_rw/work";
|
|
||||||
fs::create_dir_all(tmp_work)?;
|
|
||||||
|
|
||||||
let new_root = "/new_root";
|
|
||||||
fs::create_dir_all(new_root)?;
|
|
||||||
let overlay_opts = format!(
|
|
||||||
"lowerdir={},upperdir={},workdir={}",
|
|
||||||
image_root, tmp_upper, tmp_work
|
|
||||||
);
|
|
||||||
nix::mount::mount::<_, _, _, [u8]>(
|
|
||||||
Some("overlay-root"),
|
|
||||||
new_root,
|
|
||||||
Some("overlay"),
|
|
||||||
MsFlags::empty(),
|
|
||||||
Some(overlay_opts.as_bytes()),
|
|
||||||
)
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
Ok(new_root)
|
|
||||||
}
|
|
8
server/.gitignore
vendored
8
server/.gitignore
vendored
|
@ -1,8 +0,0 @@
|
||||||
# compiled typescript
|
|
||||||
container-baby.js
|
|
||||||
|
|
||||||
# cache, will move later
|
|
||||||
cache/
|
|
||||||
|
|
||||||
# jailer
|
|
||||||
jailer/
|
|
|
@ -1,270 +0,0 @@
|
||||||
import { execFile as _execFile } from "node:child_process";
|
|
||||||
import { access, mkdir, open, rename, writeFile } from "node:fs/promises";
|
|
||||||
import { join } from "node:path";
|
|
||||||
import { tar2squashfs } from "./tar2squashfs.js";
|
|
||||||
import { subtle } from "node:crypto";
|
|
||||||
import { Writable } from "node:stream";
|
|
||||||
|
|
||||||
type RegistryName = string;
|
|
||||||
type RegistrySecret = string;
|
|
||||||
const getToken = memoizeDownloader(_getToken);
|
|
||||||
let squashfsDownloads = new Map<string, Promise<string>>();
|
|
||||||
|
|
||||||
const cacheDir = "cache/";
|
|
||||||
// {
|
|
||||||
// const image = "gitea.nulo.in/nulo/zulip-checkin-cyborg";
|
|
||||||
// const tag = "latest";
|
|
||||||
|
|
||||||
// await downloadImage(image, tag);
|
|
||||||
// }
|
|
||||||
|
|
||||||
export const CONFIG_PATH_IN_IMAGE = "/.fireactions-image-config.json";
|
|
||||||
|
|
||||||
export function parseImageRef(ref: string): { image: string; tag: string } {
|
|
||||||
const [image, tag] = ref.split(":");
|
|
||||||
if (!image || !tag) throw new Error("Invalid image ref " + ref);
|
|
||||||
return { image, tag };
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function downloadImage(image: string, tag: string) {
|
|
||||||
await mkdir(cacheDir, { recursive: true });
|
|
||||||
const manifest = await getManifest(image, tag);
|
|
||||||
|
|
||||||
// sanity check
|
|
||||||
if (
|
|
||||||
!manifest.layers.every(
|
|
||||||
(layer) =>
|
|
||||||
layer.mediaType === "application/vnd.oci.image.layer.v1.tar" ||
|
|
||||||
layer.mediaType === "application/vnd.oci.image.layer.v1.tar+gzip"
|
|
||||||
)
|
|
||||||
) {
|
|
||||||
throw new Error("Unsupported layer");
|
|
||||||
}
|
|
||||||
|
|
||||||
console.debug(manifest);
|
|
||||||
|
|
||||||
const config = await jsonBlob(image, manifest.config.digest);
|
|
||||||
const squashfsFile = await saveSquashfs(
|
|
||||||
image,
|
|
||||||
manifest,
|
|
||||||
JSON.stringify(config, null, 2)
|
|
||||||
);
|
|
||||||
|
|
||||||
return { squashfsFile };
|
|
||||||
}
|
|
||||||
|
|
||||||
// https://stackoverflow.com/a/67600346
|
|
||||||
async function hashData(data: string, algorithm = "SHA-512"): Promise<string> {
|
|
||||||
const ec = new TextEncoder();
|
|
||||||
const digest: ArrayBuffer = await subtle.digest(algorithm, ec.encode(data));
|
|
||||||
const hashArray = Array.from(new Uint8Array(digest));
|
|
||||||
const hash = hashArray
|
|
||||||
.map((item) => item.toString(16).padStart(2, "0"))
|
|
||||||
.join("");
|
|
||||||
return hash;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function saveSquashfs(
|
|
||||||
image: string,
|
|
||||||
manifest: Manifest,
|
|
||||||
configJson: string
|
|
||||||
): Promise<string> {
|
|
||||||
const manifestDigest = await hashData(JSON.stringify(manifest));
|
|
||||||
const key = `${image.replaceAll("/", "%")}#${manifestDigest}.squashfs`;
|
|
||||||
|
|
||||||
let p = squashfsDownloads.get(key);
|
|
||||||
if (!p) {
|
|
||||||
p = (async () => {
|
|
||||||
const output = join(cacheDir, key);
|
|
||||||
const progressFile = output + ".downloading";
|
|
||||||
try {
|
|
||||||
await access(output);
|
|
||||||
// ya está cacheado
|
|
||||||
} catch {
|
|
||||||
if (process.env.DEBUG_WRITE_BLOBS)
|
|
||||||
await Promise.all(
|
|
||||||
manifest.layers.map(async (layer) => {
|
|
||||||
const res = await getBlob(image, layer.digest);
|
|
||||||
const file = await open(layer.digest, "w");
|
|
||||||
await res.body!.pipeTo(Writable.toWeb(file.createWriteStream()));
|
|
||||||
})
|
|
||||||
);
|
|
||||||
const layerStreams = manifest.layers.map(async (layer) => {
|
|
||||||
const res = await getBlob(image, layer.digest);
|
|
||||||
return res.body!;
|
|
||||||
});
|
|
||||||
await tar2squashfs(layerStreams, progressFile, [
|
|
||||||
{
|
|
||||||
content: configJson,
|
|
||||||
headers: {
|
|
||||||
name: CONFIG_PATH_IN_IMAGE,
|
|
||||||
uid: 0,
|
|
||||||
gid: 0,
|
|
||||||
mode: 0o400,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
]);
|
|
||||||
await rename(progressFile, output);
|
|
||||||
}
|
|
||||||
return output;
|
|
||||||
})();
|
|
||||||
squashfsDownloads.set(key, p);
|
|
||||||
}
|
|
||||||
|
|
||||||
return p;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function _getToken(registryUrl: string): Promise<RegistrySecret> {
|
|
||||||
const res = await fetch(`${registryUrl}/token`);
|
|
||||||
const json = await res.json();
|
|
||||||
if (
|
|
||||||
!json ||
|
|
||||||
typeof json !== "object" ||
|
|
||||||
!("token" in json) ||
|
|
||||||
typeof json.token !== "string"
|
|
||||||
)
|
|
||||||
throw new Error("Unexpected");
|
|
||||||
return json.token;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function call(registryName: string, path: string, init: RequestInit) {
|
|
||||||
const registryUrl = `https://${registryName}/v2`;
|
|
||||||
const token = await getToken(registryUrl);
|
|
||||||
console.debug(path);
|
|
||||||
const res = await fetch(`${registryUrl}${path}`, {
|
|
||||||
...init,
|
|
||||||
headers: {
|
|
||||||
...init.headers,
|
|
||||||
Authorization: `Bearer ${token}`,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
if (!res.ok) throw new Error(res.statusText);
|
|
||||||
return res;
|
|
||||||
}
|
|
||||||
|
|
||||||
// https://github.com/opencontainers/image-spec/blob/v1.0.1/image-index.md
|
|
||||||
type ManifestIndex = {
|
|
||||||
schemaVersion: 2;
|
|
||||||
mediaType: "application/vnd.oci.image.index.v1+json";
|
|
||||||
manifests: ManifestPointer[];
|
|
||||||
};
|
|
||||||
type DockerManifestV2 = {
|
|
||||||
schemaVersion: 2;
|
|
||||||
mediaType: "application/vnd.docker.distribution.manifest.v2+json";
|
|
||||||
config: Descriptor;
|
|
||||||
layers: DockerManifestV2Layer[];
|
|
||||||
};
|
|
||||||
|
|
||||||
type ManifestPointer = {
|
|
||||||
mediaType: "application/vnd.oci.image.manifest.v1+json";
|
|
||||||
digest: string;
|
|
||||||
size: number;
|
|
||||||
platform: {
|
|
||||||
architecture: "amd64" | string;
|
|
||||||
os: string;
|
|
||||||
};
|
|
||||||
annotations?: { [k: string]: string };
|
|
||||||
};
|
|
||||||
type DockerManifestV2Layer = {
|
|
||||||
mediaType: "application/vnd.docker.image.rootfs.diff.tar.gzip";
|
|
||||||
} & Descriptor;
|
|
||||||
|
|
||||||
function getImageParts(image: string): [string, string] {
|
|
||||||
const parts = image.split("/");
|
|
||||||
const registryName = parts[0];
|
|
||||||
const imgPath = parts.slice(1).join("/");
|
|
||||||
return [registryName, imgPath];
|
|
||||||
}
|
|
||||||
|
|
||||||
async function getManifest(image: string, tag: string): Promise<Manifest> {
|
|
||||||
const [registryName, imgPath] = getImageParts(image);
|
|
||||||
const res = await call(registryName, `/${imgPath}/manifests/${tag}`, {
|
|
||||||
headers: {
|
|
||||||
Accept: "application/vnd.oci.image.index.v1+json",
|
|
||||||
},
|
|
||||||
});
|
|
||||||
const json = (await res.json()) as object;
|
|
||||||
if (!("mediaType" in json)) throw new Error(`Received invalid manifest`);
|
|
||||||
|
|
||||||
if (
|
|
||||||
json.mediaType === "application/vnd.docker.distribution.manifest.v2+json"
|
|
||||||
) {
|
|
||||||
const manifest = json as DockerManifestV2;
|
|
||||||
return {
|
|
||||||
mediaType: "application/vnd.oci.image.manifest.v1+json",
|
|
||||||
config: {
|
|
||||||
...manifest.config,
|
|
||||||
mediaType: "application/vnd.oci.image.config.v1+json",
|
|
||||||
},
|
|
||||||
layers: manifest.layers.map((l) => ({
|
|
||||||
...l,
|
|
||||||
mediaType: "application/vnd.oci.image.layer.v1.tar+gzip",
|
|
||||||
})),
|
|
||||||
};
|
|
||||||
} else if (json.mediaType === "application/vnd.oci.image.manifest.v1+json") {
|
|
||||||
return json as Manifest;
|
|
||||||
} else if (json.mediaType === "application/vnd.oci.image.index.v1+json") {
|
|
||||||
const index = json as ManifestIndex;
|
|
||||||
// XXX: hardcoded amd64
|
|
||||||
const arch = "amd64";
|
|
||||||
const manifestPtr = chooseManifest(index, arch);
|
|
||||||
if (!manifestPtr) throw new Error(`No manifest for arch ${arch}`);
|
|
||||||
|
|
||||||
const manifest = await jsonBlob<Manifest>(image, manifestPtr.digest);
|
|
||||||
return manifest;
|
|
||||||
} else throw new Error(`Received invalid manifest`);
|
|
||||||
}
|
|
||||||
|
|
||||||
function chooseManifest(list: ManifestIndex, arch: string) {
|
|
||||||
console.debug(list);
|
|
||||||
return list.manifests.find(
|
|
||||||
(m) => m.platform.architecture === arch && m.platform.os === "linux"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
async function getBlob(image: string, digest: string): Promise<Response> {
|
|
||||||
const [registryName, imgPath] = getImageParts(image);
|
|
||||||
return await call(registryName, `/${imgPath}/blobs/${digest}`, {});
|
|
||||||
}
|
|
||||||
async function jsonBlob<T>(image: string, digest: string): Promise<T> {
|
|
||||||
const res = await getBlob(image, digest);
|
|
||||||
return (await res.json()) as T;
|
|
||||||
}
|
|
||||||
|
|
||||||
function memoizeDownloader<T>(
|
|
||||||
downloader: (id: string) => Promise<T>
|
|
||||||
): (id: string) => Promise<T> {
|
|
||||||
let map = new Map<string, Promise<T>>();
|
|
||||||
|
|
||||||
return (id) => {
|
|
||||||
let p = map.get(id);
|
|
||||||
if (!p) {
|
|
||||||
p = downloader(id);
|
|
||||||
map.set(id, p);
|
|
||||||
}
|
|
||||||
return p;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// https://github.com/opencontainers/image-spec/blob/v1.0.1/descriptor.md#properties
|
|
||||||
type Descriptor = {
|
|
||||||
digest: string;
|
|
||||||
size: number;
|
|
||||||
urls?: string[];
|
|
||||||
annotations?: { [k: string]: string };
|
|
||||||
};
|
|
||||||
|
|
||||||
// https://github.com/opencontainers/image-spec/blob/v1.0.1/manifest.md
|
|
||||||
type Manifest = {
|
|
||||||
mediaType: "application/vnd.oci.image.manifest.v1+json";
|
|
||||||
config: {
|
|
||||||
mediaType: "application/vnd.oci.image.config.v1+json";
|
|
||||||
} & Descriptor;
|
|
||||||
layers: ({
|
|
||||||
mediaType:
|
|
||||||
| "application/vnd.oci.image.layer.v1.tar"
|
|
||||||
| "application/vnd.oci.image.layer.v1.tar+gzip"
|
|
||||||
| "application/vnd.oci.image.layer.nondistributable.v1.tar"
|
|
||||||
| "application/vnd.oci.image.layer.nondistributable.v1.tar+gzip";
|
|
||||||
} & Descriptor)[];
|
|
||||||
};
|
|
292
server/index.js
292
server/index.js
|
@ -1,292 +0,0 @@
|
||||||
import { delay } from "nanodelay";
|
|
||||||
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 { 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);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @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") {
|
|
||||||
const image = url.searchParams.get("image");
|
|
||||||
if (!image) return res.writeHead(400).end("missing image param");
|
|
||||||
await runVm(image);
|
|
||||||
} else res.writeHead(405).end("wrong method");
|
|
||||||
} else res.writeHead(404).end("not found");
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param {string} image - ref to an OCI image
|
|
||||||
*/
|
|
||||||
async function runVm(image) {
|
|
||||||
try {
|
|
||||||
const ref = parseImageRef(image);
|
|
||||||
const { squashfsFile } = await downloadImage(ref.image, ref.tag);
|
|
||||||
await FirecrackerInstance.run({
|
|
||||||
drives: [
|
|
||||||
{
|
|
||||||
drive_id: "image",
|
|
||||||
is_read_only: true,
|
|
||||||
is_root_device: false,
|
|
||||||
path_on_host: squashfsFile,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
});
|
|
||||||
} catch (err) {
|
|
||||||
console.error(err);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @typedef {object} Drive
|
|
||||||
* @prop {string} drive_id
|
|
||||||
* @prop {string} path_on_host
|
|
||||||
* @prop {boolean} is_root_device
|
|
||||||
* @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 {
|
|
||||||
vmid;
|
|
||||||
socketPath;
|
|
||||||
proc;
|
|
||||||
/**
|
|
||||||
* @param {{ firecrackerExe: string }} param0
|
|
||||||
* @private
|
|
||||||
*/
|
|
||||||
constructor({ firecrackerExe }) {
|
|
||||||
this.vmid = jailerNanoid();
|
|
||||||
this.socketPath = join(this.chrootPath, `firecracker.sock`);
|
|
||||||
console.debug(this.socketPath);
|
|
||||||
this.proc = spawn(
|
|
||||||
"jailer",
|
|
||||||
[
|
|
||||||
...["--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) {
|
|
||||||
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",
|
|
||||||
boot_args: `console=ttyS0 reboot=k panic=1 pci=off fireactions_ip=${guestAddr}:${hostAddr}`,
|
|
||||||
});
|
|
||||||
// TODO: readonly
|
|
||||||
self.attachDrive({
|
|
||||||
drive_id: "rootfs",
|
|
||||||
path_on_host: "/rootfs.ext4",
|
|
||||||
is_root_device: true,
|
|
||||||
is_read_only: false,
|
|
||||||
});
|
|
||||||
if (opts?.drives) {
|
|
||||||
for (const drive of opts.drives) {
|
|
||||||
await self.attachDrive(drive);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
await self.request("PUT", "/network-interfaces/eth0", {
|
|
||||||
iface_id: "eth0",
|
|
||||||
guest_mac: "AA:FC:00:00:00:01",
|
|
||||||
host_dev_name: ifname,
|
|
||||||
});
|
|
||||||
|
|
||||||
// API requests are handled asynchronously, it is important the configuration is
|
|
||||||
// set, before `InstanceStart`.
|
|
||||||
// TODO: avoid race condition somehow? this is the way of the firecracker quickstart guide. check firectl/go-sdk
|
|
||||||
await delay(15);
|
|
||||||
|
|
||||||
await self.request("PUT", "/actions", {
|
|
||||||
action_type: "InstanceStart",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @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
|
|
||||||
* @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();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @type {string[]}
|
|
||||||
*/
|
|
||||||
let interfaces = [];
|
|
||||||
let ifIndex = 2;
|
|
||||||
async function createNetworkInterface() {
|
|
||||||
const ifname = "f" + nanoid(13);
|
|
||||||
await execFile("ip", ["tuntap", "add", ifname, "mode", "tap"]);
|
|
||||||
interfaces.push(ifname);
|
|
||||||
|
|
||||||
const hostAddr = `172.16.0.${ifIndex}`;
|
|
||||||
const guestAddr = `172.16.0.${ifIndex + 1}`;
|
|
||||||
ifIndex += 2;
|
|
||||||
|
|
||||||
await execFile("ip", ["addr", "add", `${hostAddr}/31`, "dev", ifname]);
|
|
||||||
await execFile("ip", ["link", "set", ifname, "up"]);
|
|
||||||
// TODO: setup masquerade
|
|
||||||
|
|
||||||
return { hostAddr, guestAddr, ifname };
|
|
||||||
}
|
|
||||||
|
|
||||||
process.on("beforeExit", async () => {
|
|
||||||
for (const ifname of interfaces) {
|
|
||||||
await execFile("ip", ["tuntap", "del", ifname, "mode", "tap"]);
|
|
||||||
}
|
|
||||||
process.exit(0);
|
|
||||||
});
|
|
||||||
|
|
||||||
server.listen("8080");
|
|
Loading…
Reference in a new issue