CRD to YAML as WASM website
A while ago, I wrote about Generating Sample YAML files from CRDs.
It’s a tool I created that lives here. It has a front-end service as well for convenience.
I wrote it in a traditional client-server manner. It’s running from a Docker Swarm container.
But, as I was thinking about it, nothing in this service requires interaction with a server. It gets some user input, processes it, and has some output. I could have written it in plain Javascript. But, since I don’t know JavaScript, or don’t know it well enough, and I do know GO, and I wanted to become more familiar with WASM, this was the perfect learning opportunity.
The bonus is that if I pull it off, it will be a static website I can serve using GitHub Pages instead of my server.
WASM
WASM has been available for a while now. You can use it with TinyGo or with plain Go or GopherJS. Each option has a significant drawback of not being able to pass in higher-order values like strings. WASM will only accept pointers to more complex variables, and that’s just a pain in the butt to deal with.
Instead, I’ve gone with another option called go-app.
Go-App
Go-App is a fantastic resource for building WASM-based SPAs. Getting started with it is a bit difficult. It’s not something that you can just straight dive into. You need to understand its flow and not force it to use a native flow for a client-server setup.
First off, it’s a SPA. The second, and maybe the most crucial part, is that it has its own update cycle of its components. That means that instead of trying to update an innerHTML or something silly like that for a SPA, we do a conditional rendering of components.
The update loop will update the components based on events and exported fields of the components. How does that look like?
Let’s take a look at a high-level index
component that is the entry point to my site:
func (i *index) Render() app.UI {
// Prevent double rendering components.
if !i.isMounted {
return app.Main()
}
return app.Main().Body(app.Div().Class("container").Body(func() app.UI {
if i.err != nil {
return app.Div().Class("container").Body(&header{}, i.buildError())
}
if i.content != nil {
return &crdView{content: i.content}
}
return app.Div().Class("container").Body(&header{}, &form{formHandler: i.OnClick})
}()))
}
Let’s break it down a bit.
First, the mount is a check, so it doesn’t double-render my components. Rendering happens when entering OnMount
event
AND when leaving. It doesn’t bother my other components, but for main
there seems to be a bug that causes some
double rending when refreshing. I solved it by adding a rendered check in my index component. Only render it once.
Second are the if
statements. I update an internal error field if there is any error during rendering or fetching
content. If that error field isn’t empty, I render an error message instead of the rest of the components. The error
message can be dismissed with an OnClick
event like this:
func (i *index) dismissError(ctx app.Context, e app.Event) {
i.err = nil
}
Basically, just clear the internal error field. This function is called in the buildError
above like this:
func (i *index) buildError() app.UI {
return app.Div().Class("alert alert-danger").Role("alert").Body(
app.Span().Class("closebtn").OnClick(i.dismissError).Body(app.Text("×")),
app.H4().Class("alert-heading").Text("Oops!"),
app.Text(i.err.Error()),
)
}
The event will ensure the Render
fires; thus, the main component is re-rendered.
Where is the WASM thing, though, that I keep talking about? That’s the best part. The go-app
uses Go’s own WASM file
building, and the wasm-js does all the heavy lifting. Internally, it will do all the transformation so you don’t have
to care about it.
Running it
To start the wasm local server, run:
make run
… from the wasm folder.
Note: I might move this folder around or rename it after writing this post.
This command will do two things. First, it will build the wasm binary. Second, it will build the main server.
The server will serve the binary and the static contents defined in the handler app.
Generating static content
It’s all nice and good, but I want statically servable content. And not another server. go-app
has our back.
To generate the static content, simply call the following function
func generateGitHubPages(h *app.Handler) {
if err := app.GenerateStaticWebsite(".", h); err != nil {
panic(err)
}
}
instead of firing off ListenAndServer
. That’s all there is to it. It will generate the following files:
tree
.
├── Makefile
├── app-worker.js
├── app.css
├── app.go
├── app.js
├── index.go
├── index.html
├── main.go
├── manifest.webmanifest
├── wasm
├── wasm_exec.js
└── web
├── app.wasm
├── css
│ ├── alert.css
│ ├── halfmoon-variables.min.css
│ ├── main.css
│ ├── prism-okaidia.css
│ ├── prism.css
│ └── root.css
├── img
│ └── logo.png
└── js
├── clipboard.min.js
├── prism.js
└── wasm_exec.js
5 directories, 22 files
The additions are app* files, index.html
, the manifest file and the wasm_exec.js
file. This folder will be the root
from which we can serve the site from GitHub pages.
GitHub Pages
All we need to do is set up some actions to serve the site data from the desired folder. And that’s it. GitHub will pick it up and serve the site correctly. I gave it a temporary home here: https://crdtoyaml.github.io/.
Pending some trouble with my DNS provider, the right location will be https://crdtoyaml.com.
Conclusion
And that’s all. My site is now static and hosted on GitHub. One less thing I need to maintain. It’s far from perfect and there are more things I want to improve in using the components correctly. And acquiring the value from the input box or the textarea could be done better.
But I’m rather proud of it anyways. And I learned a lot for future projects I would like to work on.
Thanks for reading! Gergely.