Skip to content

Commit

Permalink
feat: initial commit
Browse files Browse the repository at this point in the history
  • Loading branch information
utibeabasi6 committed Jan 13, 2024
0 parents commit a2d3d08
Show file tree
Hide file tree
Showing 10 changed files with 1,511 additions and 0 deletions.
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
cni.conf
cni-bin
dist/
2 changes: 2 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
run:
@go run .
56 changes: 56 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
# Windhoek

Windhoek is a small API written in Golang that exposes an invoke route we can call to run a function within a Firecracker VM.

# How to run
## Install dependencies

### Install Golang
```bash
pushd /tmp
wget https://go.dev/dl/go1.21.5.linux-amd64.tar.gz
rm -rf /usr/local/go && tar -C /usr/local -xzf go1.21.5.linux-amd64.tar.gz
echo "export PATH=$PATH:/usr/local/go/bin" >> ~/.bashrc
popd
```

### Install Firecracker
```bash
pushd /tmp
ARCH="$(uname -m)"
release_url="https://github.com/firecracker-microvm/firecracker/releases"
latest=$(basename $(curl -fsSLI -o /dev/null -w %{url_effective} ${release_url}/latest))
curl -L ${release_url}/download/${latest}/firecracker-${latest}-${ARCH}.tgz \
| tar -xz

# Rename the binary to "firecracker"
mv release-${latest}-$(uname -m)/firecracker-${latest}-${ARCH} /usr/local/bin/firecracker
popd
```

### Install CNI plugins
```bash
apt-get install make
git clone https://github.com/containernetworking/plugins.git /tmp/cni-plugins

# Move to the plugins directory
pushd /tmp/cni-plugins

# Build the CNI tools
./build_linux.sh

mv bin/* /opt/cni/bin

git clone https://github.com/awslabs/tc-redirect-tap
pushd tc-redirect-tap/
make install
mv tc-redirect-tap /opt/cni/bin
popd
popd
```

### Create required directories

```bash
mkdir /root/fckernels /root/fcsockets /root/fcruntimes
```
51 changes: 51 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
module github.com/runvelocity/windhoek

go 1.21.5

require (
github.com/firecracker-microvm/firecracker-go-sdk v1.0.0
github.com/labstack/echo/v4 v4.11.4
)

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/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/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/josharian/intern v1.0.0 // indirect
github.com/labstack/gommon v0.4.2 // 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.20 // 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.17.0 // indirect
golang.org/x/net v0.19.0 // indirect
golang.org/x/sys v0.15.0 // indirect
golang.org/x/text v0.14.0 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect
)
1,116 changes: 1,116 additions & 0 deletions go.sum

Large diffs are not rendered by default.

28 changes: 28 additions & 0 deletions main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package main

import (
"fmt"
"net/http"

"github.com/labstack/echo/v4"
"github.com/runvelocity/windhoek/routes"
"github.com/runvelocity/windhoek/utils"
)

type PingResponse struct {
Ok bool `json:"ok"`
}

func main() {
e := echo.New()

if err := utils.WriteCNIConfWithHostLocalSubnet(fmt.Sprintf("/etc/cni/conf.d/%s.conflist", utils.FC_NETWORK_NAME)); err != nil {
e.Logger.Fatal(err.Error())
}
e.GET("/ping", func(c echo.Context) error {
return c.JSON(http.StatusOK, PingResponse{Ok: true})
})

e.POST("/invoke", routes.InvokeFunctionHandler)
e.Logger.Fatal(e.Start(":8000"))
}
115 changes: 115 additions & 0 deletions routes/invokeFunctionHandler.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
package routes

import (
"bytes"
"encoding/json"
"fmt"
"log"
"net/http"
"os"
"time"

"github.com/labstack/echo/v4"
"github.com/runvelocity/windhoek/utils"
)

const (
MAXRETRIES = 10
BACKOFFTIME = 500
)

func InvokeFunctionHandler(c echo.Context) error {
var vmRequest utils.FirecrackerVmRequest
if err := c.Bind(&vmRequest); err != nil {
return echo.NewHTTPError(http.StatusBadRequest, err.Error())
}
vmRequest.SocketPath = fmt.Sprintf("%s/firecracker-%s.sock", utils.FC_SOCKETS_PATH, vmRequest.FunctionId)
m, ctx, err := utils.CreateVm(vmRequest)
defer func() {
err := m.Shutdown(ctx)
if err != nil {
log.Println("An error occured while shutting down vm", err)
}
err = os.Remove(vmRequest.SocketPath)
if err != nil {
log.Println("Error deleting socket file", vmRequest.SocketPath)
}
}()
if err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, fmt.Sprintf("error occured while creating vm. %s", err.Error()))
}
fmt.Println(m.Cfg.NetworkInterfaces[0].StaticConfiguration.IPConfiguration.IPAddr.IP.String())
url := fmt.Sprintf("http://%s:3000/invoke", m.Cfg.NetworkInterfaces[0].StaticConfiguration.IPConfiguration.IPAddr.IP.String())
argsJSON, err := json.Marshal(vmRequest.InvokePayload.Args)
if err != nil {
fmt.Println("Error marshaling args to JSON:", err)
errObj := utils.ErrorResponse{
Message: err.Error(),
}
return c.JSON(http.StatusInternalServerError, errObj)
}
payload := fmt.Sprintf(`{"args": %s,"handler": "%s","codeLocation": "%s"}`, argsJSON, vmRequest.InvokePayload.Handler, vmRequest.CodeLocation)

for i := 0; i < MAXRETRIES; i++ {
req, err := http.NewRequest("POST", url, bytes.NewBuffer([]byte(payload)))
if err != nil {
fmt.Println("Error creating request:", err)
errObj := utils.ErrorResponse{
Message: err.Error(),
}
return c.JSON(http.StatusInternalServerError, errObj)
}

// Set headers if needed
req.Header.Set("Content-Type", "application/json")

req.Close = true

var client = &http.Client{}
res, err := client.Do(req)

if err != nil {
time.Sleep(BACKOFFTIME * time.Millisecond)
} else {
if res.StatusCode != 200 {
var resObj map[string]interface{}
err = json.NewDecoder(res.Body).Decode(&resObj)
if err != nil {
fmt.Println("Error decoding JSON:", err)
errObj := utils.ErrorResponse{
Message: err.Error(),
}
return c.JSON(http.StatusInternalServerError, errObj)
}
if resObj["error"] != nil {
errObj := utils.ErrorResponse{
Message: resObj["error"].(string),
}
return c.JSON(res.StatusCode, errObj)
}
return c.JSON(res.StatusCode, resObj)
}
var resObj utils.FunctionInvokeResponse
err = json.NewDecoder(res.Body).Decode(&resObj)
if err != nil {
fmt.Println("Error decoding JSON:", err)
errObj := utils.ErrorResponse{
Message: err.Error(),
}
return c.JSON(http.StatusInternalServerError, errObj)
}
return c.JSON(http.StatusOK, resObj)
}
}

if err != nil {
fmt.Println("Error invoking function:", err)
errObj := utils.ErrorResponse{
Message: err.Error(),
}
return c.JSON(http.StatusInternalServerError, errObj)
}

return nil

}
77 changes: 77 additions & 0 deletions utils/config.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
package utils

import (
"fmt"
"os"

"github.com/firecracker-microvm/firecracker-go-sdk"
"github.com/firecracker-microvm/firecracker-go-sdk/client/models"
)

var (
ROOTFS_PATH = "/root/fcrootfs"
KERNEL_IMAGE_PATH = "/root/fckernels/vmlinux"
FC_SOCKETS_PATH = "/root/fcsockets"
FC_NETWORK_NAME = "fcnet"
FC_IF_NAME = "veth0"
KERNEL_ARGS = "console=ttyS0 reboot=k panic=1 pci=off"
NETWORK_MASK = "/24"
SUBNET = "192.168.127.0" + NETWORK_MASK
DEFAULT_CPU_COUNT int64 = 1
DEFAULT_MEMORY_COUNT int64 = 512
AWS_REGION = "us-east-1"
RUNTIMES map[string]string = map[string]string{
"nodejs": "/root/fcruntimes/nodejs-runtime.ext4",
}
)

func WriteCNIConfWithHostLocalSubnet(path string) error {
return os.WriteFile(path, []byte(fmt.Sprintf(
`{
"cniVersion": "1.0.0",
"name": "%s",
"plugins": [
{
"type": "ptp",
"ipMasq": true,
"ipam": {
"type": "host-local",
"subnet": "%s"
}
},
{
"type": "firewall"
},
{
"type": "tc-redirect-tap"
}
]
}`, FC_NETWORK_NAME, SUBNET)), 0644)
}

func GetVmConfig(vmRequest FirecrackerVmRequest) firecracker.Config {
drives := firecracker.NewDrivesBuilder(RUNTIMES[vmRequest.Runtime]).
// drives := firecracker.NewDrivesBuilder("/root/fcrootfs/rootfs.ext4").
Build()
networkInterface := firecracker.NetworkInterface{
CNIConfiguration: &firecracker.CNIConfiguration{
NetworkName: FC_NETWORK_NAME,
IfName: FC_IF_NAME,
},
}
cfg := firecracker.Config{
SocketPath: vmRequest.SocketPath,
KernelImagePath: KERNEL_IMAGE_PATH,
KernelArgs: KERNEL_ARGS,
Drives: drives,
MachineCfg: models.MachineConfiguration{
VcpuCount: firecracker.Int64(DEFAULT_CPU_COUNT),
MemSizeMib: firecracker.Int64(DEFAULT_MEMORY_COUNT),
},
NetworkInterfaces: []firecracker.NetworkInterface{
networkInterface,
},
}

return cfg
}
34 changes: 34 additions & 0 deletions utils/createVm.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
package utils

import (
"context"
"os"

"github.com/firecracker-microvm/firecracker-go-sdk"
)

func CreateVm(vmRequest FirecrackerVmRequest) (*firecracker.Machine, context.Context, error) {
ctx := context.Background()

cfg := GetVmConfig(vmRequest)

// Check if kernel image is readable
f, err := os.Open(cfg.KernelImagePath)
if err != nil {
return nil, ctx, err
}
defer f.Close()

cmd := firecracker.VMCommandBuilder{}.WithSocketPath(cfg.SocketPath).WithBin("/usr/local/bin/firecracker").Build(ctx)

m, err := firecracker.NewMachine(ctx, cfg, firecracker.WithProcessRunner(cmd))
if err != nil {
return nil, ctx, err
}

if err := m.Start(ctx); err != nil {
return nil, ctx, err
}

return m, ctx, nil
}
Loading

0 comments on commit a2d3d08

Please # to comment.