radu's blog

Rendering Helm templates in the browser, with WebAssembly

· Radu Matei

I’ve been trying to find a weekend to play around with Web Assembly for at least a couple of months now - I had previously read the hello world examples for both Go and Rust, but never had the time to actually try things out. So I decided to take a piece of real world Go code, that is used today in Helm, and see if I can get it to execute in the browser - I chose to replicate a simpler version of helm template, where you input the template, values file, and metadata in the web page, and the rendered template gets printed out.

This should absolutely not be used for any serious purpose, since you are about to see some glued pieces of software that happen to work. Now that we have that out of the way, let see how this works.

Interacting with the DOM from Go

The first thing you learn in JavaScript after the slight amusement of alert is gone, is to manipulate the DOM - you are able select elements by their ID or class, then read and write their values. This can be done in Go with Web Assembly using the sycall/js package, and the syntax is really similar to JavaScript:

someValue := document.Call("getElementById", "someId").Get("value").String()

This is equivalent to the following in JavaScript (and modifying the value of objects can be done in a similar way):

var someValue = document.getElementById("someId").value

So in theory, we have the inputs and outputs for our simple application - we can read the chart metadata, templates, and values files from a bunch of HTML text areas, and we can write the output to another text area. (Luckily enough, the only other HTML element we are using is a button - ideal, since that’s pretty much all I can do on the front-end.)

Now we need to actually render the Helm templates, and this is the part I wasn’t sure that was even going to compile with Web Assembly - we’re directly importing packages from k8s.io/helm into our application, as well as the library for unmarshaling YAML, then calling the Helm render engine:

func render(this js.Value, args []js.Value) interface{} {
	document := js.Global().Get("document")
	metadata := document.Call("getElementById", "metadata").Get("value").String()
	templates := document.Call("getElementById", "templates").Get("value").String()
	values := document.Call("getElementById", "values").Get("value").String()

    m := &chart.Metadata{}
    // unmarshaling the chart metadata, coming from the HTML input
	err := yaml.Unmarshal([]byte(metadata), m)
	if err != nil {
		fmt.Printf("cannot unmarshal chart metadata: %v", err)
	}

    t := []*chart.Template{}
    // Helm templates, coming from the HTML input
	t = append(t, &chart.Template{Name: "service.yaml", Data: []byte(templates)})
	c := &chart.Chart{
		Metadata:  m,
		Templates: t,
    }

    // values file, from the HTML input
	config := &chart.Config{Raw: values, Values: map[string]*chart.Value{}}

	renderedTemplates, err := renderutil.Render(c, config, renderOpts)
	if err != nil {
		fmt.Printf("error rendering: %v", err)
	}
	fmt.Printf("rendered: %v", renderedTemplates)

	result := ""
	for _, v := range renderedTemplates {
		result += v
	}

    // showing the result in the browser
	document.Call("getElementById", "result").Set("textContent", result)
	return nil
}

The next important piece is registering a function written in Go so it can be used from JavaScript:

js.Global().Set("render", js.FuncOf(render))

The only thing to note here is the signature for render, which has to be func render(this js.Value, args []js.Value) interface{}, where this is the JavaScript context that calls the Go function, and args contains the arguments the function was called with.

Complete documentation for the syscall/js package.

In this article we’re not really using this or args, but we are directly reading the values of HTML elements from the DOM.

You can find the complete repository for this example on GitHub.

If you want to get started with Go and Web Assembly, the best resource is from the official Go repository.

Building and using the package

In the same way we cross-compile our Go projects for various operating systems and architectures, we need to compile our package for Web Assembly (in this example, wasm/render.go is the path to the Go file we have above):

GOARCH=wasm GOOS=js go build -o lib.wasm wasm/render.go

The output of the compilation is a lib.wasm file we will import in our web application.

lib.wasm needs to be imported in the web application, and the module needs to be instantiated - this can be found it index.html in the repository, and is mostly the standard way of importing and starting a Web Assembly module for Go.

Next, we need the text areas and the button:

Metadata:
<textarea id="metadata" cols="50" rows="10" charswidth="100"></textarea>
Templates:
<textarea id="templates" cols="50" rows="20" charswidth="100"></textarea>
Values:
<textarea id="values" cols="50" rows="20" charswidth="100"></textarea>

<button onClick="render()" id="renderButton">
  Render
</button>
Result:
<textarea id="result" cols="50" rows="20" charswidth="100"></textarea>

The callback for the renderButton, render(), is precisely the Go function from above, and will be executed whenever the button is clicked.

Provided we included the wasm_exec.js script and used it to fetch the lib.wasm library we just built, then we should be able to test this out - in the repo for this example there is a simply web server - go run main.go will start listening on port 8080, and accessing localhost:8080 will serve index.html, which loads lib.wasm and registers the render function so that whenever the button is pressed, the Go function gets executed and renders the Helm templates:

Conclusion

In around 40 lines of code I managed to get package from a fairly complex project written in Go, and execute it in the browser, with Web Assembly. Do I really understand how it works? Not yet, but this is incredibly exciting, both from the technical perspective, but just as important, because of all the possibilities it opens up.

Thanks for reading!