Wasm Builders 🧱

Cover image for Capsule, my personal wasm multi-tools knife (part 1)
Philippe Charrière
Philippe Charrière

Posted on • Updated on

Capsule, my personal wasm multi-tools knife (part 1)

Sept 4th, 2022 - update following release 0.1.9

Well, my vacation is over 😢.
Most of the time, on vacation, I like to take advantage of my time to learn new things, improve my skills, experiment...🧪

I had just discovered the Wazero project ("the zero dependency WebAssembly runtime for Go developers"). It was an opportunity to progress in Go and continue experimenting with WebAssembly and WASI.

So I decided to make a small CLI (with Go) to run wasm modules developed with TinyGo (only).

I decided to give new powers to my little project so that it could "serve" wasm services via http. Wazero is exciting, and I was able to quickly add host functions (👀 see: https://www.wasm.builders/k33g_org/extend-wasm-with-host-functions-thanks-to-wazero-3n0n). Of course, my inspiration comes entirely from the Sat project of Suborbital. This was the birth of the "Capsule" project, my personal wasm multi-tools knife.

As a TAM (technical account manager) at GitLab, I plan to use it for real for my hands-on demos on GitLab CI/CD.

So, let me introduce you "Capsule".

Install Capsule

Download the appropriate release of Capsule:

and install it:

sudo tar -zxf \
capsule-${CAPSULE_VERSION}-${OS}-${ARCH}.tar.gz \
--directory /usr/local/bin
Enter fullscreen mode Exit fullscreen mode

Check the Capsule install:

capsule version
Enter fullscreen mode Exit fullscreen mode

You should get: v0.1.9 🐞[ladybug]

Use Capsule as a CLI

To build a WASI module, you need to install TinyGo. But if you want to test Capsule easily without installing anything, I created a Gitpod project on GitHub: https://github.com/bots-garden/capsule-launcher-demo, use this url to open it directly with Gitpod: https://gitpod.io/#https://github.com/bots-garden/capsule-launcher-demo

Create your first TinyGo WASI module

First, create a go.mod file:

go mod init hola
Enter fullscreen mode Exit fullscreen mode

Then, create a hola.go file:

package main

//1️⃣
import hf "github.com/bots-garden/capsule/capsulemodule/hostfunctions"

// main is required.
func main() {

    hf.Log("🚀 ignition...") //2️⃣
    hf.SetHandle(Handle) //🖐3️⃣
}

func Handle(params []string) (string, error) { //4️⃣

    // get the value of an environment variable
    message, err := hf.GetEnv("MESSAGE") //5️⃣
    if err != nil {
        hf.Log(err.Error())
    } else {
        hf.Log("MESSAGE=" + message)
    }

    // display the list of the arguments
    for _, param := range params {
        hf.Log("- parameter is: " + param)
    }

    // create a text file
        //6️⃣
    newFile, err := hf.WriteFile("hello.txt", "👋 HELLO WORLD 🌍") 
    if err != nil {
        hf.Log(err.Error())
    }
    hf.Log(newFile)

    // read the content of the file
    txt, err := hf.ReadFile("hello.txt") //7️⃣
    if err != nil {
        hf.Log(err.Error())
    }
    hf.Log(txt)

    return "that's all 🙂", err
}
Enter fullscreen mode Exit fullscreen mode
  • 1️⃣: import the host functions of Capsule.
  • 2️⃣: hf.Log prints a message.
  • 3️⃣: hf.SetHandle(Handle) is mandatory (Capsule must know which function to run).
  • 4️⃣: the parameter of Handle is always an array of strings and you must return a string and an error.
  • 5️⃣: hf.GetEnv allows to read environment variables
  • 6️⃣: hf.WriteFile writes a text file
  • 7️⃣: hf.ReadFile reads a text file

Then build the module:

go mod tidy
tinygo build -o hola.wasm -scheduler=none -target wasi ./hola.go
Enter fullscreen mode Exit fullscreen mode

Run the module:

MESSAGE="🎉 tada!" capsule \
  -mode=cli \
  -wasm=hola.wasm \
  "👋 hello world 🌍" "I 💜 wasm"
Enter fullscreen mode Exit fullscreen mode

You will get this:

🚀 ignition...
MESSAGE=🎉 tada!
- parameter is: 👋 hello world 🌍
- parameter is: I 💜 wasm
file created
👋 HELLO WORLD 🌍
that's all 🙂
Enter fullscreen mode Exit fullscreen mode

You can notice that Capsule is very easy to use to test WASI TinyGo module. I plan to develop a Rust version, but I need to find time.

Use Capsule as an HTTP server

Serving (with HTTP) a WASI TinyGo Capsule module is not more challenging.

Create a new TinyGo WASI module

First, create a go.mod file:

go mod init hey
Enter fullscreen mode Exit fullscreen mode

Then, create a hey.go file:

package main

import (
    hf "github.com/bots-garden/capsule/capsulemodule/hostfunctions"
    /* string to json */
    "github.com/tidwall/gjson"
    /* create json string */
    "github.com/tidwall/sjson"
)

// main is required.
func main() {
    hf.SetHandleHttp(Handle) //1️⃣
}

//2️⃣
func Handle(req hf.Request) (resp hf.Response, errResp error) {

    // display the body request
    hf.Log("📝 body: " + req.Body)

    // get the data of the body request
    author := gjson.Get(req.Body, "author")
    message := gjson.Get(req.Body, "message")

    hf.Log("🟢 Content-Type: " + req.Headers["Content-Type"])
    hf.Log("🔵 Content-Length: " + req.Headers["Content-Length"])
    hf.Log("🟠 User-Agent: " + req.Headers["User-Agent"])
    hf.Log("🔴 My-Token: " + req.Headers["My-Token"])

    headers := map[string]string{ //3️⃣
        "Content-Type": "application/json; charset=utf-8",
        "YourMessage":  message.String(),
        "MyToken":      req.Headers["My-Token"],
    }

    jsondoc := `{"message": ""}`
    jsondoc, err := sjson.Set(jsondoc, "message", "👋 hey! "+author.String()+" What's up?")

    return hf.Response{Body: jsondoc, Headers: headers}, err //4️⃣
}
Enter fullscreen mode Exit fullscreen mode
  • I use gjson and sjson to handle JSON.
  • 1️⃣: hf.SetHandleHttp(Handle) is mandatory (Capsule must know which function to serve) 🖐 notice that you must use SetHandleHttp and not SetHandle.
  • 2️⃣: the parameter of Handle is always a hf.Request type () with a Body: string field (the body request) and a Headers map[string]string field (the headers' request) and you must return a hf.Response type () with a Body: string field (the response), and a Headers map[string]string field (the headers' response) and an error.
  • 3️⃣: set the headers of the response.
  • 4️⃣: return the data.
  • (*) see request.go and response.go

Then build the module:

go mod tidy
tinygo build -o hey.wasm -scheduler=none -target wasi ./hey.go
Enter fullscreen mode Exit fullscreen mode

To serve the module, use the below command:

capsule \
  -wasm=hey.wasm \
  -mode=http \
  -httpPort=7070
Enter fullscreen mode Exit fullscreen mode

Then you can query you new "wasm nano service":

curl -v -X POST \
  http://localhost:7070 \
  -H 'content-type: application/json; charset=utf-8' \
  -H 'my-token: I love Pandas' \
  -d '{"message": "Golang 💜 wasm", "author": "k33g"}'
Enter fullscreen mode Exit fullscreen mode

On the server side the output will be:

📝 body: {"author":"k33g","message":"Golang 💜 wasm"}
🟢 Content-Type: application/json; charset=utf-8
🔵 Content-Length: 49
🟠 User-Agent: curl/7.68.0
🔴 My-Token: I love Pandas
Enter fullscreen mode Exit fullscreen mode

The output of the curl command will be:

< Content-Type: application/json; charset=utf-8
< Mytoken: I love Pandas
< Yourmessage: Golang 💜 wasm
< Date: Wed, 17 Aug 2022 07:07:12 GMT
< Content-Length: 39
< 
* Connection #0 to host localhost left intact
{"message":"👋 hey! k33g What's up?"}
Enter fullscreen mode Exit fullscreen mode

So, it's easy to create quickly a small HTTP service with TinyGo and Capsule.

One more thing before moving to the next topic. Capsule can download the wasm module from a remote location. For example, provide the wasm file with an HTTP server by running this command at the root of your project:

python3 -m http.server 9090
Enter fullscreen mode Exit fullscreen mode

And now, you can run your module like that:

capsule \
  -url=http://localhost:9090/hey.wasm \ #1️⃣
  -wasm=./tmp/hey.wasm \ #2️⃣
  -mode=http \
  -httpPort=7070
Enter fullscreen mode Exit fullscreen mode
  • 1️⃣: the remote location of the wasm module.
  • 2️⃣: where to download the module

Use Capsule to serve HTML

When I'm demoing GitLab CI/CD, it's always fancier to show an HTML page in a browser than a JSON payload in a terminal. So, it's possible to serve HTML with Capsule (it's very experimental, don't expect to serve a website).

Create a new TinyGo WASI module

First, create a go.mod file:

go mod init hello
Enter fullscreen mode Exit fullscreen mode

Then, create a hello.go file:

package main

import (
    hf "github.com/bots-garden/capsule/capsulemodule/hostfunctions"
)

func main() {
    hf.SetHandleHttp(Handle)
}

func Handle(req hf.Request) (resp hf.Response, errResp error) {

    message, _ := hf.GetEnv("MESSAGE")
    html := `
    <html>
            <head>
                <meta charset="utf-8">
                <title>Wasm is fantastic 😍</title>

                <meta name="viewport" content="width=device-width, initial-scale=1">

                <style>
                    .container { min-height: 100vh; display: flex; justify-content: center; align-items: center; text-align: center; }
                    .title { font-family: "Source Sans Pro", "Helvetica Neue", Arial, sans-serif; display: block; font-weight: 300; font-size: 100px; color: #35495e; letter-spacing: 1px; }
                    .subtitle { font-family: "Source Sans Pro", "Helvetica Neue", Arial, sans-serif; font-weight: 300; font-size: 42px; color: #526488; word-spacing: 5px; padding-bottom: 15px; }
                </style>

            </head>

            <body>
                <section class="container">
                    <div>
                        <h1 class="title">` + message + `</h1>
                        <h2 class="subtitle">Served with 💜 by Capsule 💊</h2>
                    </div>
                </section>
            </body>

    </html>
    `

    headers := map[string]string{
        "Content-Type": "text/html; charset=utf-8",
    }

    return hf.Response{Body: html, Headers: headers}, nil
}
Enter fullscreen mode Exit fullscreen mode

Then build the module:

go mod tidy
tinygo build -o hello.wasm -scheduler=none -target wasi ./hello.go
Enter fullscreen mode Exit fullscreen mode

To serve the module, use the below command:

MESSAGE="👋 hello world 🌍" capsule \
  -wasm=hello.wasm \
  -mode=http \
  -httpPort=7071
Enter fullscreen mode Exit fullscreen mode

Then, open this URL: http://localhost:7071 to get this amazing web page:

Capsule Web Page

My plans with Capsule in a near future

I plan to work on Capsule to improve my Go and Wasm skills, to learn new things. So, I will add new features (like NATS support). I'm already using it for my work (service deployment, webhooks, bots,...)

Going further with Capsule

I already added some other host functions to Capsule. Right now a Capsule TinyGo Wasm Module can:

  • Print a message
  • Read and Write text files
  • Read value of the environment variables
  • Make HTTP requests (GET & POST)
  • Make Redis queries (GET & SET)
  • Make CouchBase N1QL Query (😍 my favourite host function, I will work more on it, so stay tuned)

You can read more about these functions here: https://github.com/bots-garden/capsule#host-functions

Going even further with Capsule

This will be part of a future article. But, the more I advanced, the more I found new use cases (still about my CI/CD demonstrations).

Then, I found myself developing (quickly and in draft mode for the moment) a reverse proxy, a wasm registry and finally, a worker to deploy my wasm functions remotely.

By the way, I used the Gin Web Framework for all of this, and Gin is fabulous.

I hope you will give it a try (and try Wazero too). And see you soon for the next episode.

Photo by little plant on Unsplash

Oldest comments (0)