Compare commits
10 commits
08097035e4
...
d876b46145
Author | SHA1 | Date | |
---|---|---|---|
d876b46145 | |||
f480b54259 | |||
e8658c6201 | |||
6d483c3e8b | |||
a46881fcbe | |||
e3995c1926 | |||
87e1a509a8 | |||
5bc228f7ab | |||
9dcd3c7279 | |||
2599602d58 |
26 changed files with 1584 additions and 177 deletions
7
.gitignore
vendored
7
.gitignore
vendored
|
@ -2,4 +2,11 @@ rootfs.ext4
|
||||||
rootfs.qcow2
|
rootfs.qcow2
|
||||||
fireactions
|
fireactions
|
||||||
*.ext4
|
*.ext4
|
||||||
|
*.squashfs
|
||||||
|
*.config.json
|
||||||
vmlinux.bin
|
vmlinux.bin
|
||||||
|
node_modules/
|
||||||
|
|
||||||
|
*.log
|
||||||
|
|
||||||
|
build.cjs
|
||||||
|
|
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.");
|
||||||
|
}
|
117
agent/main.go
117
agent/main.go
|
@ -1,117 +0,0 @@
|
||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
"errors"
|
|
||||||
"io"
|
|
||||||
"io/ioutil"
|
|
||||||
"log"
|
|
||||||
"net/http"
|
|
||||||
"os"
|
|
||||||
"os/exec"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"github.com/labstack/echo/v4"
|
|
||||||
"github.com/labstack/echo/v4/middleware"
|
|
||||||
)
|
|
||||||
|
|
||||||
func main() {
|
|
||||||
sec, err := parseSecret()
|
|
||||||
if err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
e := echo.New()
|
|
||||||
|
|
||||||
e.Use(middleware.Logger())
|
|
||||||
e.Use(middleware.Recover())
|
|
||||||
e.Use(auth{secret: sec}.middleware)
|
|
||||||
|
|
||||||
e.GET("/hello", hello)
|
|
||||||
e.POST("/run", run)
|
|
||||||
|
|
||||||
e.Logger.Fatal(e.Start(":8080"))
|
|
||||||
}
|
|
||||||
|
|
||||||
func hello(c echo.Context) error {
|
|
||||||
return c.String(http.StatusOK, "Hello, World!")
|
|
||||||
}
|
|
||||||
|
|
||||||
type runResp struct {
|
|
||||||
Stdout string
|
|
||||||
Stderr string
|
|
||||||
Error string
|
|
||||||
}
|
|
||||||
|
|
||||||
func run(c echo.Context) error {
|
|
||||||
f, err := os.CreateTemp("", "fireactions-agent-*")
|
|
||||||
if err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
if _, err = io.Copy(f, c.Request().Body); err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
if err = f.Close(); err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if err = os.Chmod(f.Name(), 0700); err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
cmd := exec.Command(f.Name())
|
|
||||||
var stderr bytes.Buffer
|
|
||||||
var stdout bytes.Buffer
|
|
||||||
cmd.Stdout = &stdout
|
|
||||||
cmd.Stderr = &stderr
|
|
||||||
err = cmd.Run()
|
|
||||||
errorr := ""
|
|
||||||
if err != nil {
|
|
||||||
log.Println(err)
|
|
||||||
errorr = err.Error()
|
|
||||||
}
|
|
||||||
return c.JSON(http.StatusOK, runResp{
|
|
||||||
Stdout: string(stdout.Bytes()),
|
|
||||||
Stderr: string(stderr.Bytes()),
|
|
||||||
Error: errorr,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
type auth struct {
|
|
||||||
secret string
|
|
||||||
}
|
|
||||||
|
|
||||||
func (a auth) middleware(next echo.HandlerFunc) echo.HandlerFunc {
|
|
||||||
return func(c echo.Context) error {
|
|
||||||
s := strings.Split(c.Request().Header.Get("Authorization"), " ")
|
|
||||||
if len(s) < 2 {
|
|
||||||
return c.String(http.StatusBadRequest, "fuck no")
|
|
||||||
}
|
|
||||||
sec := s[1]
|
|
||||||
if sec == a.secret {
|
|
||||||
return next(c)
|
|
||||||
} else {
|
|
||||||
return c.String(http.StatusUnauthorized, "wrong secret")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func parseSecret() (string, error) {
|
|
||||||
byt, err := ioutil.ReadFile("/proc/cmdline")
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
opts := strings.Split(string(byt), " ")
|
|
||||||
for _, opt := range opts {
|
|
||||||
s := strings.Split(opt, "=")
|
|
||||||
key := s[0]
|
|
||||||
val := s[1]
|
|
||||||
if key == "fireactions.secret" {
|
|
||||||
if len(val) < 5 {
|
|
||||||
return "", errors.New("secret too short")
|
|
||||||
}
|
|
||||||
return val, nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return "", errors.New("no secret in cmdline")
|
|
||||||
}
|
|
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
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,57 +0,0 @@
|
||||||
#!/bin/sh -xe
|
|
||||||
# https://github.com/firecracker-microvm/firecracker/blob/main/docs/rootfs-and-kernel-setup.md
|
|
||||||
|
|
||||||
dir="$(mktemp --tmpdir -d tmp.fireactions-rootfs.XXXXXXXXXX)"
|
|
||||||
podman run -it --rm -v "$dir:/rootfs:Z" docker.io/alpine:3.18 sh -c "
|
|
||||||
mkdir -p /rootfs/etc/apk
|
|
||||||
cp -r /etc/apk/keys /rootfs/etc/apk/
|
|
||||||
echo https://dl-cdn.alpinelinux.org/alpine/v3.18/main >> /rootfs/etc/apk/repositories
|
|
||||||
echo https://dl-cdn.alpinelinux.org/alpine/v3.18/community >> /rootfs/etc/apk/repositories
|
|
||||||
apk add --initdb --root /rootfs alpine-base dropbear util-linux dropbear-dbclient dhcpcd
|
|
||||||
"
|
|
||||||
|
|
||||||
# gotta go fast
|
|
||||||
echo 'rc_parallel="YES"' >> "$dir"/etc/rc.conf
|
|
||||||
|
|
||||||
mkdir -p "$dir"/usr/local/sbin
|
|
||||||
go build -tags=netgo -o "$dir"/usr/local/sbin/fireactions-agent ./agent
|
|
||||||
# https://github.com/OpenRC/openrc/blob/master/service-script-guide.md
|
|
||||||
echo "#!/sbin/openrc-run
|
|
||||||
pidfile=\"/run/\${RC_SVCNAME}.pid\"
|
|
||||||
command_background=true
|
|
||||||
command=/usr/local/sbin/fireactions-agent" > "$dir"/etc/init.d/fireactions-agent
|
|
||||||
chmod +x "$dir"/etc/init.d/fireactions-agent
|
|
||||||
ln -s /etc/init.d/fireactions-agent "$dir"/etc/runlevels/default/
|
|
||||||
|
|
||||||
ln -s /etc/init.d/devfs "$dir"/etc/runlevels/boot/
|
|
||||||
ln -s /etc/init.d/procfs "$dir"/etc/runlevels/boot/
|
|
||||||
ln -s /etc/init.d/sysfs "$dir"/etc/runlevels/boot/
|
|
||||||
ln -s /etc/init.d/networking "$dir"/etc/runlevels/default/
|
|
||||||
# ln -s /etc/init.d/dhcpcd "$dir"/etc/runlevels/default/
|
|
||||||
ln -s /etc/init.d/dropbear "$dir"/etc/runlevels/default/
|
|
||||||
|
|
||||||
echo ttyS0 > "$dir"/etc/securetty
|
|
||||||
ln -s agetty "$dir"/etc/init.d/agetty.ttyS0
|
|
||||||
ln -s /etc/init.d/agetty.ttyS0 "$dir"/etc/runlevels/default/
|
|
||||||
# ln -s /etc/init.d/local "$dir"/etc/runlevels/default/
|
|
||||||
|
|
||||||
mkdir -p "$dir"/etc/dropbear
|
|
||||||
for t in rsa dss ed25519 ecdsa; do
|
|
||||||
dropbearkey -t $t -f "$dir/etc/dropbear/dropbear_${t}_host_key"
|
|
||||||
done
|
|
||||||
|
|
||||||
sudo mkdir -p "$dir"/root/.ssh
|
|
||||||
echo ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIEhrcTPMrhrOdwpgFkRFjTt8rdxt3gg16LJvBCZENRWa user@personal | sudo tee "$dir"/root/.ssh/authorized_keys
|
|
||||||
echo "auto lo
|
|
||||||
iface lo inet loopback" > "$dir"/etc/network/interfaces
|
|
||||||
|
|
||||||
rm rootfs.ext4
|
|
||||||
fallocate --length 1G rootfs.ext4
|
|
||||||
mkfs.ext4 rootfs.ext4
|
|
||||||
|
|
||||||
mkdir -p /tmp/rootfs
|
|
||||||
sudo mount rootfs.ext4 /tmp/rootfs
|
|
||||||
sudo cp -r "$dir"/* /tmp/rootfs/
|
|
||||||
sudo umount /tmp/rootfs
|
|
||||||
|
|
||||||
# sudo rm -rf "$dir"
|
|
5
js/.gitignore
vendored
Normal file
5
js/.gitignore
vendored
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
# compiled typescript
|
||||||
|
container-baby.js
|
||||||
|
|
||||||
|
# cache, will move later
|
||||||
|
cache/
|
246
js/container-baby.ts
Normal file
246
js/container-baby.ts
Normal file
|
@ -0,0 +1,246 @@
|
||||||
|
import { execFile as _execFile } from "node:child_process";
|
||||||
|
import { access, mkdir, writeFile } from "node:fs/promises";
|
||||||
|
import { join } from "node:path";
|
||||||
|
import { tar2squashfs } from "./tar2squashfs.js";
|
||||||
|
import { subtle } from "node:crypto";
|
||||||
|
|
||||||
|
type RegistryName = string;
|
||||||
|
type RegistrySecret = string;
|
||||||
|
const getToken = memoizeDownloader(_getToken);
|
||||||
|
let squashfsDownloads = new Map<string, Promise<string>>();
|
||||||
|
|
||||||
|
const tmpDir = "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(tmpDir, { recursive: true });
|
||||||
|
const manifest = await getContainerManifest(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(tmpDir, key);
|
||||||
|
try {
|
||||||
|
await access(output);
|
||||||
|
// ya está cacheado
|
||||||
|
} catch {
|
||||||
|
const layerStreams = manifest.layers.map(async (layer) => {
|
||||||
|
const res = await getBlob(image, layer.digest);
|
||||||
|
return res.body!;
|
||||||
|
});
|
||||||
|
await tar2squashfs(layerStreams, output, [
|
||||||
|
{
|
||||||
|
content: configJson,
|
||||||
|
headers: {
|
||||||
|
name: CONFIG_PATH_IN_IMAGE,
|
||||||
|
uid: 0,
|
||||||
|
gid: 0,
|
||||||
|
mode: 0o400,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
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)[];
|
||||||
|
};
|
||||||
|
|
||||||
|
async function getManifest(image: string, ptr: ManifestPointer) {
|
||||||
|
return await jsonBlob<Manifest>(image, ptr.digest);
|
||||||
|
}
|
29
js/fetch-hack.d.ts
vendored
Normal file
29
js/fetch-hack.d.ts
vendored
Normal file
|
@ -0,0 +1,29 @@
|
||||||
|
// https://stackoverflow.com/a/75676044
|
||||||
|
|
||||||
|
import * as undici_types from "undici";
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
export const {
|
||||||
|
fetch,
|
||||||
|
FormData,
|
||||||
|
Headers,
|
||||||
|
Request,
|
||||||
|
Response,
|
||||||
|
}: typeof import("undici");
|
||||||
|
|
||||||
|
type FormData = undici_types.FormData;
|
||||||
|
type Headers = undici_types.Headers;
|
||||||
|
type HeadersInit = undici_types.HeadersInit;
|
||||||
|
type BodyInit = undici_types.BodyInit;
|
||||||
|
type Request = undici_types.Request;
|
||||||
|
type RequestInit = undici_types.RequestInit;
|
||||||
|
type RequestInfo = undici_types.RequestInfo;
|
||||||
|
type RequestMode = undici_types.RequestMode;
|
||||||
|
type RequestRedirect = undici_types.RequestRedirect;
|
||||||
|
type RequestCredentials = undici_types.RequestCredentials;
|
||||||
|
type RequestDestination = undici_types.RequestDestination;
|
||||||
|
type ReferrerPolicy = undici_types.ReferrerPolicy;
|
||||||
|
type Response = undici_types.Response;
|
||||||
|
type ResponseInit = undici_types.ResponseInit;
|
||||||
|
type ResponseType = undici_types.ResponseType;
|
||||||
|
}
|
175
js/index.js
Normal file
175
js/index.js
Normal file
|
@ -0,0 +1,175 @@
|
||||||
|
import { delay } from "nanodelay";
|
||||||
|
import { nanoid } from "nanoid";
|
||||||
|
import { spawn } from "node:child_process";
|
||||||
|
import { createServer, request as _request } from "node:http";
|
||||||
|
import { tmpdir } from "node:os";
|
||||||
|
import { join } from "node:path";
|
||||||
|
import { downloadImage, parseImageRef } from "./container-baby.js";
|
||||||
|
|
||||||
|
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
|
||||||
|
*/
|
||||||
|
|
||||||
|
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",
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* @param [opts] {{ drives?: Drive[] }}
|
||||||
|
*/
|
||||||
|
static async run(opts) {
|
||||||
|
const self = new FirecrackerInstance();
|
||||||
|
await delay(15);
|
||||||
|
|
||||||
|
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,
|
||||||
|
});
|
||||||
|
if (opts?.drives) {
|
||||||
|
for (const drive of opts.drives) {
|
||||||
|
await self.request("PUT", "/drives/" + drive.drive_id, drive);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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");
|
26
js/package.json
Normal file
26
js/package.json
Normal file
|
@ -0,0 +1,26 @@
|
||||||
|
{
|
||||||
|
"name": "js",
|
||||||
|
"type": "module",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"description": "",
|
||||||
|
"main": "index.js",
|
||||||
|
"scripts": {
|
||||||
|
"start": "esbuild --bundle index.js --platform=node --sourcemap > build.cjs && node --enable-source-maps build.cjs"
|
||||||
|
},
|
||||||
|
"keywords": [],
|
||||||
|
"author": "",
|
||||||
|
"license": "ISC",
|
||||||
|
"devDependencies": {
|
||||||
|
"@tsconfig/node16": "^1.0.4",
|
||||||
|
"@types/gunzip-maybe": "^1.4.0",
|
||||||
|
"@types/node": "^20.2.5",
|
||||||
|
"@types/tar-stream": "^2.2.2",
|
||||||
|
"undici": "^5.22.1"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"gunzip-maybe": "^1.4.2",
|
||||||
|
"nanodelay": "^2.0.2",
|
||||||
|
"nanoid": "^4.0.2",
|
||||||
|
"tar-stream": "^3.0.0"
|
||||||
|
}
|
||||||
|
}
|
311
js/pnpm-lock.yaml
Normal file
311
js/pnpm-lock.yaml
Normal file
|
@ -0,0 +1,311 @@
|
||||||
|
lockfileVersion: '6.1'
|
||||||
|
|
||||||
|
settings:
|
||||||
|
autoInstallPeers: true
|
||||||
|
excludeLinksFromLockfile: false
|
||||||
|
|
||||||
|
dependencies:
|
||||||
|
gunzip-maybe:
|
||||||
|
specifier: ^1.4.2
|
||||||
|
version: 1.4.2
|
||||||
|
nanodelay:
|
||||||
|
specifier: ^2.0.2
|
||||||
|
version: 2.0.2
|
||||||
|
nanoid:
|
||||||
|
specifier: ^4.0.2
|
||||||
|
version: 4.0.2
|
||||||
|
tar-stream:
|
||||||
|
specifier: ^3.0.0
|
||||||
|
version: 3.0.0
|
||||||
|
|
||||||
|
devDependencies:
|
||||||
|
'@tsconfig/node16':
|
||||||
|
specifier: ^1.0.4
|
||||||
|
version: 1.0.4
|
||||||
|
'@types/gunzip-maybe':
|
||||||
|
specifier: ^1.4.0
|
||||||
|
version: 1.4.0
|
||||||
|
'@types/node':
|
||||||
|
specifier: ^20.2.5
|
||||||
|
version: 20.2.5
|
||||||
|
'@types/tar-stream':
|
||||||
|
specifier: ^2.2.2
|
||||||
|
version: 2.2.2
|
||||||
|
undici:
|
||||||
|
specifier: ^5.22.1
|
||||||
|
version: 5.22.1
|
||||||
|
|
||||||
|
packages:
|
||||||
|
|
||||||
|
/@tsconfig/node16@1.0.4:
|
||||||
|
resolution: {integrity: sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==}
|
||||||
|
dev: true
|
||||||
|
|
||||||
|
/@types/gunzip-maybe@1.4.0:
|
||||||
|
resolution: {integrity: sha512-dFP9GrYAR9KhsjTkWJ8q8Gsfql75YIKcg9DuQOj/IrlPzR7W+1zX+cclw1McV82UXAQ+Lpufvgk3e9bC8+HzgA==}
|
||||||
|
dependencies:
|
||||||
|
'@types/node': 20.2.5
|
||||||
|
dev: true
|
||||||
|
|
||||||
|
/@types/node@20.2.5:
|
||||||
|
resolution: {integrity: sha512-JJulVEQXmiY9Px5axXHeYGLSjhkZEnD+MDPDGbCbIAbMslkKwmygtZFy1X6s/075Yo94sf8GuSlFfPzysQrWZQ==}
|
||||||
|
dev: true
|
||||||
|
|
||||||
|
/@types/tar-stream@2.2.2:
|
||||||
|
resolution: {integrity: sha512-1AX+Yt3icFuU6kxwmPakaiGrJUwG44MpuiqPg4dSolRFk6jmvs4b3IbUol9wKDLIgU76gevn3EwE8y/DkSJCZQ==}
|
||||||
|
dependencies:
|
||||||
|
'@types/node': 20.2.5
|
||||||
|
dev: true
|
||||||
|
|
||||||
|
/abort-controller@3.0.0:
|
||||||
|
resolution: {integrity: sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==}
|
||||||
|
engines: {node: '>=6.5'}
|
||||||
|
dependencies:
|
||||||
|
event-target-shim: 5.0.1
|
||||||
|
dev: false
|
||||||
|
|
||||||
|
/b4a@1.6.4:
|
||||||
|
resolution: {integrity: sha512-fpWrvyVHEKyeEvbKZTVOeZF3VSKKWtJxFIxX/jaVPf+cLbGUSitjb49pHLqPV2BUNNZ0LcoeEGfE/YCpyDYHIw==}
|
||||||
|
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:
|
||||||
|
resolution: {integrity: sha512-19OEpq7vWgsH6WkvkBJQDFvJS1uPcbFOQ4v9CU839dO+ZZXUZO6XpE6hNCqvlIIj+4fZvRiJ6DsAQ382GwiyTQ==}
|
||||||
|
dependencies:
|
||||||
|
pako: 0.2.9
|
||||||
|
dev: false
|
||||||
|
|
||||||
|
/buffer-from@1.1.2:
|
||||||
|
resolution: {integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==}
|
||||||
|
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:
|
||||||
|
resolution: {integrity: sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==}
|
||||||
|
engines: {node: '>=10.16.0'}
|
||||||
|
dependencies:
|
||||||
|
streamsearch: 1.1.0
|
||||||
|
dev: true
|
||||||
|
|
||||||
|
/core-util-is@1.0.3:
|
||||||
|
resolution: {integrity: sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==}
|
||||||
|
dev: false
|
||||||
|
|
||||||
|
/duplexify@3.7.1:
|
||||||
|
resolution: {integrity: sha512-07z8uv2wMyS51kKhD1KsdXJg5WQ6t93RneqRxUHnskXVtlYYkLqM0gqStQZ3pj073g687jPCHrqNfCzawLYh5g==}
|
||||||
|
dependencies:
|
||||||
|
end-of-stream: 1.4.4
|
||||||
|
inherits: 2.0.4
|
||||||
|
readable-stream: 2.3.8
|
||||||
|
stream-shift: 1.0.1
|
||||||
|
dev: false
|
||||||
|
|
||||||
|
/end-of-stream@1.4.4:
|
||||||
|
resolution: {integrity: sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==}
|
||||||
|
dependencies:
|
||||||
|
once: 1.4.0
|
||||||
|
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:
|
||||||
|
resolution: {integrity: sha512-NcvQXt7Cky1cNau15FWy64IjuO8X0JijhTBBrJj1YlxlDfRkJXNaK9RFUjwpfDPzMdv7wB38jr53l9tkNLxnWg==}
|
||||||
|
dev: false
|
||||||
|
|
||||||
|
/gunzip-maybe@1.4.2:
|
||||||
|
resolution: {integrity: sha512-4haO1M4mLO91PW57BMsDFf75UmwoRX0GkdD+Faw+Lr+r/OZrOCS0pIBwOL1xCKQqnQzbNFGgK2V2CpBUPeFNTw==}
|
||||||
|
hasBin: true
|
||||||
|
dependencies:
|
||||||
|
browserify-zlib: 0.1.4
|
||||||
|
is-deflate: 1.0.0
|
||||||
|
is-gzip: 1.0.0
|
||||||
|
peek-stream: 1.1.3
|
||||||
|
pumpify: 1.5.1
|
||||||
|
through2: 2.0.5
|
||||||
|
dev: false
|
||||||
|
|
||||||
|
/ieee754@1.2.1:
|
||||||
|
resolution: {integrity: sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==}
|
||||||
|
dev: false
|
||||||
|
|
||||||
|
/inherits@2.0.4:
|
||||||
|
resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==}
|
||||||
|
dev: false
|
||||||
|
|
||||||
|
/is-deflate@1.0.0:
|
||||||
|
resolution: {integrity: sha512-YDoFpuZWu1VRXlsnlYMzKyVRITXj7Ej/V9gXQ2/pAe7X1J7M/RNOqaIYi6qUn+B7nGyB9pDXrv02dsB58d2ZAQ==}
|
||||||
|
dev: false
|
||||||
|
|
||||||
|
/is-gzip@1.0.0:
|
||||||
|
resolution: {integrity: sha512-rcfALRIb1YewtnksfRIHGcIY93QnK8BIQ/2c9yDYcG/Y6+vRoJuTWBmmSEbyLLYtXm7q35pHOHbZFQBaLrhlWQ==}
|
||||||
|
engines: {node: '>=0.10.0'}
|
||||||
|
dev: false
|
||||||
|
|
||||||
|
/isarray@1.0.0:
|
||||||
|
resolution: {integrity: sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==}
|
||||||
|
dev: false
|
||||||
|
|
||||||
|
/nanodelay@2.0.2:
|
||||||
|
resolution: {integrity: sha512-6AS5aCSXsjoxq2Jr9CdaAeT60yoYDOTp6po9ziqeOeY6vf6uTEHYSqWql6EFILrM3fEfXgkZ4KqE9L0rTm/wlA==}
|
||||||
|
engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0}
|
||||||
|
dev: false
|
||||||
|
|
||||||
|
/nanoid@4.0.2:
|
||||||
|
resolution: {integrity: sha512-7ZtY5KTCNheRGfEFxnedV5zFiORN1+Y1N6zvPTnHQd8ENUvfaDBeuJDZb2bN/oXwXxu3qkTXDzy57W5vAmDTBw==}
|
||||||
|
engines: {node: ^14 || ^16 || >=18}
|
||||||
|
hasBin: true
|
||||||
|
dev: false
|
||||||
|
|
||||||
|
/once@1.4.0:
|
||||||
|
resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==}
|
||||||
|
dependencies:
|
||||||
|
wrappy: 1.0.2
|
||||||
|
dev: false
|
||||||
|
|
||||||
|
/pako@0.2.9:
|
||||||
|
resolution: {integrity: sha512-NUcwaKxUxWrZLpDG+z/xZaCgQITkA/Dv4V/T6bw7VON6l1Xz/VnrBqrYjZQ12TamKHzITTfOEIYUj48y2KXImA==}
|
||||||
|
dev: false
|
||||||
|
|
||||||
|
/peek-stream@1.1.3:
|
||||||
|
resolution: {integrity: sha512-FhJ+YbOSBb9/rIl2ZeE/QHEsWn7PqNYt8ARAY3kIgNGOk13g9FGyIY6JIl/xB/3TFRVoTv5as0l11weORrTekA==}
|
||||||
|
dependencies:
|
||||||
|
buffer-from: 1.1.2
|
||||||
|
duplexify: 3.7.1
|
||||||
|
through2: 2.0.5
|
||||||
|
dev: false
|
||||||
|
|
||||||
|
/process-nextick-args@2.0.1:
|
||||||
|
resolution: {integrity: sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==}
|
||||||
|
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:
|
||||||
|
resolution: {integrity: sha512-ruPMNRkN3MHP1cWJc9OWr+T/xDP0jhXYCLfJcBuX54hhfIBnaQmAUMfDcG4DM5UMWByBbJY69QSphm3jtDKIkA==}
|
||||||
|
dependencies:
|
||||||
|
end-of-stream: 1.4.4
|
||||||
|
once: 1.4.0
|
||||||
|
dev: false
|
||||||
|
|
||||||
|
/pumpify@1.5.1:
|
||||||
|
resolution: {integrity: sha512-oClZI37HvuUJJxSKKrC17bZ9Cu0ZYhEAGPsPUy9KlMUmv9dKX2o77RUmq7f3XjIxbwyGwYzbzQ1L2Ks8sIradQ==}
|
||||||
|
dependencies:
|
||||||
|
duplexify: 3.7.1
|
||||||
|
inherits: 2.0.4
|
||||||
|
pump: 2.0.1
|
||||||
|
dev: false
|
||||||
|
|
||||||
|
/queue-tick@1.0.1:
|
||||||
|
resolution: {integrity: sha512-kJt5qhMxoszgU/62PLP1CJytzd2NKetjSRnyuj31fDd3Rlcz3fzlFdFLD1SItunPwyqEOkca6GbV612BWfaBag==}
|
||||||
|
dev: false
|
||||||
|
|
||||||
|
/readable-stream@2.3.8:
|
||||||
|
resolution: {integrity: sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==}
|
||||||
|
dependencies:
|
||||||
|
core-util-is: 1.0.3
|
||||||
|
inherits: 2.0.4
|
||||||
|
isarray: 1.0.0
|
||||||
|
process-nextick-args: 2.0.1
|
||||||
|
safe-buffer: 5.1.2
|
||||||
|
string_decoder: 1.1.1
|
||||||
|
util-deprecate: 1.0.2
|
||||||
|
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:
|
||||||
|
resolution: {integrity: sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==}
|
||||||
|
dev: false
|
||||||
|
|
||||||
|
/stream-shift@1.0.1:
|
||||||
|
resolution: {integrity: sha512-AiisoFqQ0vbGcZgQPY1cdP2I76glaVA/RauYR4G4thNFgkTqr90yXTo4LYX60Jl+sIlPNHHdGSwo01AvbKUSVQ==}
|
||||||
|
dev: false
|
||||||
|
|
||||||
|
/streamsearch@1.1.0:
|
||||||
|
resolution: {integrity: sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==}
|
||||||
|
engines: {node: '>=10.0.0'}
|
||||||
|
dev: true
|
||||||
|
|
||||||
|
/streamx@2.15.0:
|
||||||
|
resolution: {integrity: sha512-HcxY6ncGjjklGs1xsP1aR71INYcsXFJet5CU1CHqihQ2J5nOsbd4OjgjHO42w/4QNv9gZb3BueV+Vxok5pLEXg==}
|
||||||
|
dependencies:
|
||||||
|
fast-fifo: 1.2.0
|
||||||
|
queue-tick: 1.0.1
|
||||||
|
dev: false
|
||||||
|
|
||||||
|
/string_decoder@1.1.1:
|
||||||
|
resolution: {integrity: sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==}
|
||||||
|
dependencies:
|
||||||
|
safe-buffer: 5.1.2
|
||||||
|
dev: false
|
||||||
|
|
||||||
|
/tar-stream@3.0.0:
|
||||||
|
resolution: {integrity: sha512-O6OfUKBbQOqAhh6owTWmA730J/yZCYcpmZ1DBj2YX51ZQrt7d7NgzrR+CnO9wP6nt/viWZW2XeXLavX3/ZEbEg==}
|
||||||
|
dependencies:
|
||||||
|
b4a: 1.6.4
|
||||||
|
bl: 6.0.2
|
||||||
|
streamx: 2.15.0
|
||||||
|
dev: false
|
||||||
|
|
||||||
|
/through2@2.0.5:
|
||||||
|
resolution: {integrity: sha512-/mrRod8xqpA+IHSLyGCQ2s8SPHiCDEeQJSep1jqLYeEUClOFG2Qsh+4FU6G9VeqpZnGW/Su8LQGc4YKni5rYSQ==}
|
||||||
|
dependencies:
|
||||||
|
readable-stream: 2.3.8
|
||||||
|
xtend: 4.0.2
|
||||||
|
dev: false
|
||||||
|
|
||||||
|
/undici@5.22.1:
|
||||||
|
resolution: {integrity: sha512-Ji2IJhFXZY0x/0tVBXeQwgPlLWw13GVzpsWPQ3rV50IFMMof2I55PZZxtm4P6iNq+L5znYN9nSTAq0ZyE6lSJw==}
|
||||||
|
engines: {node: '>=14.0'}
|
||||||
|
dependencies:
|
||||||
|
busboy: 1.6.0
|
||||||
|
dev: true
|
||||||
|
|
||||||
|
/util-deprecate@1.0.2:
|
||||||
|
resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==}
|
||||||
|
dev: false
|
||||||
|
|
||||||
|
/wrappy@1.0.2:
|
||||||
|
resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==}
|
||||||
|
dev: false
|
||||||
|
|
||||||
|
/xtend@4.0.2:
|
||||||
|
resolution: {integrity: sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==}
|
||||||
|
engines: {node: '>=0.4'}
|
||||||
|
dev: false
|
79
js/tar2squashfs.js
Normal file
79
js/tar2squashfs.js
Normal file
|
@ -0,0 +1,79 @@
|
||||||
|
// TODO: sandboxear este proceso (¿seccomp? ¿minijail?)
|
||||||
|
// TODO: sandboxear mkquashfs
|
||||||
|
// TODO: fijarse como hace firecracker-containerd
|
||||||
|
// TODO: fijarse como hace [ignite](https://github.com/weaveworks/ignite)
|
||||||
|
|
||||||
|
import gunzip from "gunzip-maybe";
|
||||||
|
import { spawn } from "node:child_process";
|
||||||
|
import { Duplex, Writable } from "node:stream";
|
||||||
|
import { ReadableStream } from "node:stream/web";
|
||||||
|
import { extract, pack } from "tar-stream";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Takes many tar streams and converts them to a squashfs image.
|
||||||
|
*
|
||||||
|
* ## Why?
|
||||||
|
*
|
||||||
|
* We need a way to download OCI images (composed of layers that are tarballs) to something
|
||||||
|
* that can be accessed inside a VM. I wanted to not need to copy the image each time a VM
|
||||||
|
* is made. So, having a local HTTP cache and having the agent inside the VM download it into
|
||||||
|
* a temporary filesystem would copy it. This is bad for the life of an SSD, slow for an HDD,
|
||||||
|
* and too much to save into RAM for each VM.
|
||||||
|
*
|
||||||
|
* Instead, we download the images before booting the VM and package the layers up into a
|
||||||
|
* squashfs image that can be mounted inside the VM. This way, we reuse downloaded images
|
||||||
|
* efficiently.
|
||||||
|
*
|
||||||
|
* @param {Promise<ReadableStream>[]} streams
|
||||||
|
* @param {string} output
|
||||||
|
* @param {{ content: string, headers: import("tar-stream").Headers }[]} extraFiles
|
||||||
|
*/
|
||||||
|
export async function tar2squashfs(streams, output, extraFiles) {
|
||||||
|
const child = spawn(
|
||||||
|
"mksquashfs",
|
||||||
|
[
|
||||||
|
"-",
|
||||||
|
output,
|
||||||
|
"-tar",
|
||||||
|
...["-comp", "zstd"],
|
||||||
|
...["-Xcompression-level", "3"],
|
||||||
|
],
|
||||||
|
{
|
||||||
|
// stdio: "pipe",
|
||||||
|
stdio: ["pipe", "inherit", "inherit"],
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
const p = pack();
|
||||||
|
p.pipe(child.stdin);
|
||||||
|
p.on("error", console.error);
|
||||||
|
|
||||||
|
for (const streamP of streams) {
|
||||||
|
const stream = await streamP;
|
||||||
|
const ex = extract();
|
||||||
|
|
||||||
|
ex.on("entry", (header, stream, next) => {
|
||||||
|
stream.pipe(p.entry(header, next));
|
||||||
|
});
|
||||||
|
|
||||||
|
stream.pipeThrough(Duplex.toWeb(gunzip())).pipeTo(Writable.toWeb(ex));
|
||||||
|
|
||||||
|
await new Promise((resolve) =>
|
||||||
|
ex.on("finish", () => {
|
||||||
|
resolve(void 0);
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const { headers, content } of extraFiles) {
|
||||||
|
p.entry(headers, content);
|
||||||
|
}
|
||||||
|
|
||||||
|
p.finalize();
|
||||||
|
|
||||||
|
await new Promise((resolve, reject) =>
|
||||||
|
child.on("close", (code) => {
|
||||||
|
code === 0 ? resolve(void 0) : reject(code);
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
11
js/tsconfig.json
Normal file
11
js/tsconfig.json
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
{
|
||||||
|
"extends": "@tsconfig/node16/tsconfig.json",
|
||||||
|
"compilerOptions": {
|
||||||
|
"allowJs": true,
|
||||||
|
"checkJs": true,
|
||||||
|
"lib": ["es2023"],
|
||||||
|
"module": "Node16",
|
||||||
|
"target": "es2022",
|
||||||
|
"noEmit": true
|
||||||
|
}
|
||||||
|
}
|
7
main.go
7
main.go
|
@ -82,15 +82,16 @@ func startVM() (string, agentConfig, *firecracker.Machine) {
|
||||||
VcpuCount: firecracker.Int64(1),
|
VcpuCount: firecracker.Int64(1),
|
||||||
MemSizeMib: firecracker.Int64(1024),
|
MemSizeMib: firecracker.Int64(1024),
|
||||||
},
|
},
|
||||||
KernelArgs: "fireactions.secret=" + secret,
|
// TODO: setup jailer
|
||||||
|
KernelArgs: "console=ttyS0 reboot=k panic=1 pci=off fireactions.secret=" + secret,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO: change stdout/stderr files
|
||||||
stdoutPath := "/tmp/stdout.log"
|
stdoutPath := "/tmp/stdout.log"
|
||||||
stdout, err := os.OpenFile(stdoutPath, os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0644)
|
stdout, err := os.OpenFile(stdoutPath, os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0644)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(fmt.Errorf("failed to create stdout file: %v", err))
|
panic(fmt.Errorf("failed to create stdout file: %v", err))
|
||||||
}
|
}
|
||||||
|
|
||||||
stderrPath := "/tmp/stderr.log"
|
stderrPath := "/tmp/stderr.log"
|
||||||
stderr, err := os.OpenFile(stderrPath, os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0644)
|
stderr, err := os.OpenFile(stderrPath, os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0644)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -169,7 +170,7 @@ func (a agentConfig) run(script []byte) error {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
log.Println(string(byt))
|
log.Println("[QIOdLqxLoKIMQ0uGoxNuu]", string(byt))
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
12
readme.md
12
readme.md
|
@ -4,3 +4,15 @@
|
||||||
sudo env GOBIN=/opt/cni/bin go install github.com/awslabs/tc-redirect-tap/cmd/tc-redirect-tap@latest
|
sudo env GOBIN=/opt/cni/bin go install github.com/awslabs/tc-redirect-tap/cmd/tc-redirect-tap@latest
|
||||||
sudo dnf install containernetworking-plugins
|
sudo dnf install containernetworking-plugins
|
||||||
```
|
```
|
||||||
|
|
||||||
|
```sh
|
||||||
|
# build rootfs image (incl. agent)
|
||||||
|
(cd rootfs; pnpm install)
|
||||||
|
node rootfs/index.js
|
||||||
|
# build & run daemon
|
||||||
|
go build && sudo ./fireactions
|
||||||
|
# run VM
|
||||||
|
curl --data-raw "#!/bin/sh"\n"echo hi" localhost:8080/run
|
||||||
|
# inspect VM tty
|
||||||
|
tail -f /tmp/std*
|
||||||
|
```
|
||||||
|
|
1
rootfs/.npmrc
Normal file
1
rootfs/.npmrc
Normal file
|
@ -0,0 +1 @@
|
||||||
|
@nulo:registry=https://gitea.nulo.in/api/packages/nulo/npm/
|
153
rootfs/build.js
Normal file
153
rootfs/build.js
Normal file
|
@ -0,0 +1,153 @@
|
||||||
|
// @ts-check
|
||||||
|
import { init } from "@nulo/apkit";
|
||||||
|
import { execFile as _execFile } from "node:child_process";
|
||||||
|
import {
|
||||||
|
chmod,
|
||||||
|
copyFile,
|
||||||
|
lstat,
|
||||||
|
mkdir,
|
||||||
|
mkdtemp,
|
||||||
|
readFile,
|
||||||
|
readdir,
|
||||||
|
rm,
|
||||||
|
writeFile,
|
||||||
|
} from "node:fs/promises";
|
||||||
|
import { tmpdir } from "node:os";
|
||||||
|
import { join } from "node:path";
|
||||||
|
import { promisify } from "node:util";
|
||||||
|
const execFile = promisify(_execFile);
|
||||||
|
|
||||||
|
const root = await mkdtemp(join(tmpdir(), "fireactions-rootfs."));
|
||||||
|
const alpine = await init(root);
|
||||||
|
await alpine.install(["util-linux", "dhcpcd", "eudev"]);
|
||||||
|
|
||||||
|
const initPath = await buildInit();
|
||||||
|
await copyFile(initPath, r("/sbin/fireactions-init"));
|
||||||
|
|
||||||
|
await writeSbin(
|
||||||
|
r("/sbin/init"),
|
||||||
|
`#!/bin/sh
|
||||||
|
/etc/init-files/00-pseudofs.sh
|
||||||
|
/etc/init-files/05-misc.sh
|
||||||
|
|
||||||
|
#exec /sbin/fireactions-init
|
||||||
|
/sbin/fireactions-init || sh
|
||||||
|
`
|
||||||
|
);
|
||||||
|
|
||||||
|
await mkdir(r("/etc/init-files/"));
|
||||||
|
await writeSbin(
|
||||||
|
r("/etc/init-files/00-pseudofs.sh"),
|
||||||
|
`#!/bin/sh
|
||||||
|
mountpoint -q /proc || mount -o nosuid,noexec,nodev -t proc proc /proc
|
||||||
|
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
|
||||||
|
mountpoint -q /dev || mount -o mode=0755,nosuid -t devtmpfs dev /dev
|
||||||
|
mkdir -p -m0755 /run/runit /run/lvm /run/user /run/lock /run/log /dev/pts /dev/shm
|
||||||
|
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
|
||||||
|
mountpoint -q /sys/kernel/security || mount -n -t securityfs securityfs /sys/kernel/security
|
||||||
|
`
|
||||||
|
);
|
||||||
|
|
||||||
|
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"),
|
||||||
|
// ]);
|
||||||
|
|
||||||
|
const ext4 = "rootfs.ext4";
|
||||||
|
await rm(ext4);
|
||||||
|
await execFile("fallocate", ["--length", "1G", ext4]);
|
||||||
|
await execFile("mkfs.ext4", [ext4]);
|
||||||
|
|
||||||
|
const mountpoint = await mkdtemp(join(tmpdir(), "tmp.fireactions-mountpoint."));
|
||||||
|
try {
|
||||||
|
await mkdirp(mountpoint);
|
||||||
|
await execFile("sudo", ["mount", ext4, mountpoint]);
|
||||||
|
try {
|
||||||
|
for (const dir of await readdir(root)) {
|
||||||
|
await execFile("sudo", ["cp", "-r", r(dir), mountpoint]);
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
await execFile("sudo", ["umount", mountpoint]);
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
await rm(mountpoint, { recursive: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
// # sudo rm -rf "$dir"
|
||||||
|
|
||||||
|
// helpers
|
||||||
|
/**
|
||||||
|
* @param {import("node:fs").PathLike} path
|
||||||
|
* @param {string} content
|
||||||
|
* @returns {Promise<void>}
|
||||||
|
*/
|
||||||
|
async function append(path, content) {
|
||||||
|
const file = await readFile(path, "utf-8");
|
||||||
|
await writeFile(path, file + "\n" + content);
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* @param {import("node:fs").PathLike} path
|
||||||
|
* @returns {Promise<void>}
|
||||||
|
*/
|
||||||
|
async function mkdirp(path) {
|
||||||
|
await mkdir(path, { recursive: true });
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* @param {string} path
|
||||||
|
*/
|
||||||
|
function r(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
Normal file
1
rootfs/init/.gitignore
vendored
Normal file
|
@ -0,0 +1 @@
|
||||||
|
target/
|
126
rootfs/init/Cargo.lock
generated
Normal file
126
rootfs/init/Cargo.lock
generated
Normal file
|
@ -0,0 +1,126 @@
|
||||||
|
# This file is automatically @generated by Cargo.
|
||||||
|
# It is not intended for manual editing.
|
||||||
|
version = 3
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "bitflags"
|
||||||
|
version = "1.3.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
|
||||||
|
|
||||||
|
[[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 = "init"
|
||||||
|
version = "0.1.0"
|
||||||
|
dependencies = [
|
||||||
|
"nix",
|
||||||
|
"serde",
|
||||||
|
"serde_json",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[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 = "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 = "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 = "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 = "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 = "unicode-ident"
|
||||||
|
version = "1.0.9"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "b15811caf2415fb889178633e7724bad2509101cde276048e013b9def5e51fa0"
|
11
rootfs/init/Cargo.toml
Normal file
11
rootfs/init/Cargo.toml
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
[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"
|
52
rootfs/init/src/main.rs
Normal file
52
rootfs/init/src/main.rs
Normal file
|
@ -0,0 +1,52 @@
|
||||||
|
use nix::mount::MsFlags;
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use std::fs;
|
||||||
|
use std::{env, io, 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: Vec<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, Debug)]
|
||||||
|
struct ImageConfig {
|
||||||
|
architecture: String,
|
||||||
|
config: Config,
|
||||||
|
}
|
||||||
|
|
||||||
|
fn main() -> Result<(), io::Error> {
|
||||||
|
let src_path = "/image_root";
|
||||||
|
fs::create_dir_all(src_path)?;
|
||||||
|
|
||||||
|
nix::mount::mount::<_, _, _, [u8]>(
|
||||||
|
Some("/dev/vdb"),
|
||||||
|
src_path,
|
||||||
|
Some("squashfs"),
|
||||||
|
MsFlags::MS_RDONLY,
|
||||||
|
None,
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let config_file = Path::new(&src_path).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(src_path)?;
|
||||||
|
env::set_current_dir("/")?;
|
||||||
|
|
||||||
|
let mut child = process::Command::new(&config.config.cmd[0])
|
||||||
|
.args(&config.config.cmd[1..])
|
||||||
|
.env_clear()
|
||||||
|
.envs(config.config.env.iter().map(|s| s.split_once('=').unwrap()))
|
||||||
|
.spawn()?;
|
||||||
|
let status = child.wait()?;
|
||||||
|
if !status.success() {
|
||||||
|
println!("{}", status);
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
20
rootfs/package.json
Normal file
20
rootfs/package.json
Normal file
|
@ -0,0 +1,20 @@
|
||||||
|
{
|
||||||
|
"name": "fireactions-rootfs",
|
||||||
|
"type": "module",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"description": "",
|
||||||
|
"main": "index.js",
|
||||||
|
"scripts": {
|
||||||
|
"test": "echo \"Error: no test specified\" && exit 1"
|
||||||
|
},
|
||||||
|
"keywords": [],
|
||||||
|
"author": "",
|
||||||
|
"license": "ISC",
|
||||||
|
"dependencies": {
|
||||||
|
"@nulo/apkit": "https://gitea.nulo.in/Nulo/apkit/archive/hackyshit.tar.gz"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@tsconfig/node16": "^1.0.4",
|
||||||
|
"@types/node": "^20.2.5"
|
||||||
|
}
|
||||||
|
}
|
34
rootfs/pnpm-lock.yaml
Normal file
34
rootfs/pnpm-lock.yaml
Normal file
|
@ -0,0 +1,34 @@
|
||||||
|
lockfileVersion: '6.1'
|
||||||
|
|
||||||
|
settings:
|
||||||
|
autoInstallPeers: true
|
||||||
|
excludeLinksFromLockfile: false
|
||||||
|
|
||||||
|
dependencies:
|
||||||
|
'@nulo/apkit':
|
||||||
|
specifier: https://gitea.nulo.in/Nulo/apkit/archive/hackyshit.tar.gz
|
||||||
|
version: '@gitea.nulo.in/Nulo/apkit/archive/hackyshit.tar.gz'
|
||||||
|
|
||||||
|
devDependencies:
|
||||||
|
'@tsconfig/node16':
|
||||||
|
specifier: ^1.0.4
|
||||||
|
version: 1.0.4
|
||||||
|
'@types/node':
|
||||||
|
specifier: ^20.2.5
|
||||||
|
version: 20.2.5
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
'@gitea.nulo.in/Nulo/apkit/archive/hackyshit.tar.gz':
|
||||||
|
resolution: {tarball: https://gitea.nulo.in/Nulo/apkit/archive/hackyshit.tar.gz}
|
||||||
|
name: '@nulo/apkit'
|
||||||
|
version: 0.0.1
|
||||||
|
dev: false
|
8
rootfs/tsconfig.json
Normal file
8
rootfs/tsconfig.json
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
{
|
||||||
|
"extends": "@tsconfig/node16/tsconfig.json",
|
||||||
|
"exclude": ["build.js/", "build/"],
|
||||||
|
"compilerOptions": {
|
||||||
|
"allowJs": true,
|
||||||
|
"checkJs": true
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in a new issue