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:
- capsule-0.1.9-darwin-amd64.tar.gz
- capsule-0.1.9-darwin-arm64.tar.gz
- capsule-0.1.9-linux-amd64.tar.gz
- capsule-0.1.9-linux-arm64.tar.gz
and install it:
sudo tar -zxf \
capsule-${CAPSULE_VERSION}-${OS}-${ARCH}.tar.gz \
--directory /usr/local/bin
Check the Capsule install:
capsule version
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
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
}
- 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 astring
and anerror
.- 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
Run the module:
MESSAGE="🎉 tada!" capsule \
-mode=cli \
-wasm=hola.wasm \
"👋 hello world 🌍" "I 💜 wasm"
You will get this:
🚀 ignition...
MESSAGE=🎉 tada!
- parameter is: 👋 hello world 🌍
- parameter is: I 💜 wasm
file created
👋 HELLO WORLD 🌍
that's all 🙂
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
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️⃣
}
- 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 useSetHandleHttp
and notSetHandle
.- 2️⃣: the parameter of
Handle
is always ahf.Request
type () with aBody: string
field (the body request) and aHeaders map[string]string
field (the headers' request) and you must return ahf.Response
type () with aBody: string
field (the response), and aHeaders map[string]string
field (the headers' response) and anerror
.- 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
To serve the module, use the below command:
capsule \
-wasm=hey.wasm \
-mode=http \
-httpPort=7070
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"}'
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
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?"}
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
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
- 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
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
}
Then build the module:
go mod tidy
tinygo build -o hello.wasm -scheduler=none -target wasi ./hello.go
To serve the module, use the below command:
MESSAGE="👋 hello world 🌍" capsule \
-wasm=hello.wasm \
-mode=http \
-httpPort=7071
Then, open this URL: http://localhost:7071 to get this amazing 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
Top comments (0)