Compare commits
7 commits
d876b46145
...
67784da199
Author | SHA1 | Date | |
---|---|---|---|
67784da199 | |||
5ed0f62abf | |||
e4302b6c1f | |||
165bf72672 | |||
63690627d6 | |||
ec3a75acca | |||
a46cc0d308 |
19 changed files with 26 additions and 1656 deletions
7
.gitignore
vendored
7
.gitignore
vendored
|
@ -1,12 +1,5 @@
|
||||||
rootfs.ext4
|
rootfs.ext4
|
||||||
rootfs.qcow2
|
|
||||||
fireactions
|
|
||||||
*.ext4
|
|
||||||
*.squashfs
|
|
||||||
*.config.json
|
|
||||||
vmlinux.bin
|
vmlinux.bin
|
||||||
node_modules/
|
node_modules/
|
||||||
|
|
||||||
*.log
|
|
||||||
|
|
||||||
build.cjs
|
build.cjs
|
||||||
|
|
184
agent/index.js
184
agent/index.js
|
@ -1,184 +0,0 @@
|
||||||
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.");
|
|
||||||
}
|
|
|
@ -1,22 +0,0 @@
|
||||||
{
|
|
||||||
"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"
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,59 +0,0 @@
|
||||||
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
|
|
|
@ -1,8 +0,0 @@
|
||||||
{
|
|
||||||
"extends": "@tsconfig/node16/tsconfig.json",
|
|
||||||
"exclude": ["build.js/", "build/"],
|
|
||||||
"compilerOptions": {
|
|
||||||
"allowJs": true,
|
|
||||||
"checkJs": true
|
|
||||||
}
|
|
||||||
}
|
|
51
go.mod
51
go.mod
|
@ -1,51 +0,0 @@
|
||||||
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
|
|
||||||
)
|
|
203
main.go
203
main.go
|
@ -1,203 +0,0 @@
|
||||||
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
|
|
||||||
}
|
|
29
readme.md
29
readme.md
|
@ -1,18 +1,27 @@
|
||||||
## setup
|
# fireactions
|
||||||
|
|
||||||
```sh
|
¡WIP!
|
||||||
sudo env GOBIN=/opt/cni/bin go install github.com/awslabs/tc-redirect-tap/cmd/tc-redirect-tap@latest
|
|
||||||
sudo dnf install containernetworking-plugins
|
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.
|
||||||
```
|
|
||||||
|
[firecracker]: <https://github.com/firecracker-microvm/firecracker>
|
||||||
|
[Fly Machines]: <https://fly.io/docs/reference/machines/>
|
||||||
|
|
||||||
|
## setup
|
||||||
|
|
||||||
```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
|
||||||
# build & run daemon
|
# run daemon
|
||||||
go build && sudo ./fireactions
|
(cd server; pnpm install && pnpm start) &
|
||||||
# run VM
|
# run VM
|
||||||
curl --data-raw "#!/bin/sh"\n"echo hi" localhost:8080/run
|
curl "localhost:8080/run?image=gitea.nulo.in/nulo/super-image:3.18"
|
||||||
# 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.
|
||||||
|
|
||||||
|
|
|
@ -82,6 +82,7 @@ hostname -F /etc/hostname
|
||||||
// r("/root/.ssh/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]);
|
||||||
|
|
|
@ -48,5 +48,8 @@ fn main() -> Result<(), io::Error> {
|
||||||
if !status.success() {
|
if !status.success() {
|
||||||
println!("{}", status);
|
println!("{}", status);
|
||||||
}
|
}
|
||||||
|
nix::sys::reboot::reboot(nix::sys::reboot::RebootMode::RB_AUTOBOOT)
|
||||||
|
.map(|_| {})
|
||||||
|
.unwrap();
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
0
js/.gitignore → server/.gitignore
vendored
0
js/.gitignore → server/.gitignore
vendored
0
js/fetch-hack.d.ts → server/fetch-hack.d.ts
vendored
0
js/fetch-hack.d.ts → server/fetch-hack.d.ts
vendored
|
@ -80,6 +80,7 @@ class FirecrackerInstance {
|
||||||
*/
|
*/
|
||||||
static async run(opts) {
|
static async run(opts) {
|
||||||
const self = new FirecrackerInstance();
|
const self = new FirecrackerInstance();
|
||||||
|
// TODO: retry until success
|
||||||
await delay(15);
|
await delay(15);
|
||||||
|
|
||||||
await self.request("PUT", "/boot-source", {
|
await self.request("PUT", "/boot-source", {
|
||||||
|
@ -90,6 +91,7 @@ class FirecrackerInstance {
|
||||||
drive_id: "rootfs",
|
drive_id: "rootfs",
|
||||||
path_on_host: "../rootfs.ext4",
|
path_on_host: "../rootfs.ext4",
|
||||||
is_root_device: true,
|
is_root_device: true,
|
||||||
|
// TODO: readonly
|
||||||
is_read_only: false,
|
is_read_only: false,
|
||||||
});
|
});
|
||||||
if (opts?.drives) {
|
if (opts?.drives) {
|
||||||
|
@ -100,6 +102,7 @@ class FirecrackerInstance {
|
||||||
|
|
||||||
// API requests are handled asynchronously, it is important the configuration is
|
// API requests are handled asynchronously, it is important the configuration is
|
||||||
// set, before `InstanceStart`.
|
// 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 delay(15);
|
||||||
|
|
||||||
await self.request("PUT", "/actions", {
|
await self.request("PUT", "/actions", {
|
Loading…
Reference in a new issue