Introduction to khadga
This book is a small guide on how to create a web application using react, typescript, and webassembly generated from rust. It will take you from knowing nothing about how to create wasm to how to generate both the front and back end code for your app.
The application that is generated here is a small chat application that will set up WebSocket connections between two or more clients and the central web server.
If you're wondering where the name khadga comes from, it's a Sanskrit word meaning sword. It is often referred to in spiritual or mystical concepts as the sword that cuts away illusion.
Why?
One might ask why go through this? If the main point of this app is to do a chat application with video, there's a dime-a-dozen ready made apps for that.
Basically, I wanted to write a non-trivial application from top to bottom. A truly full-stack application where the front end, the back end, the database, the CI deployments, integration with IoT sensor data, and the data analysis is all done by one developer (that'd be me, and hopefully you the reader as well).
Yes, it's a tall order. Although the primary purpose is to use this project as a vehicle for learning how to do deep learning, my intention is to use khadga as a learning tool. Not just for myself but for others as well. What I have learned is that most books don't walk you through a non-trivial project from beginning to end. Or, they might show you how to use some framework, but not always why.
My hope is that by me forging ahead and suffering the learning pains, others can follow along and avoid the mistakes I made. Because of the pedantic nature of this project, I will endeavor to do the following:
- Write documentation for all my code
- Keep this book up to date
- Show you every step of building a project (as much as possible)
- Writing unit and integration tests
- Using CI/CD to build, test, and deploy an application (on openshift)
- Try not to cut corners (ie, I will try to use
.unwrap()
or.expect()
as little as I can)
What you will learn
This guide will walk you through everything required to develop, test and deploy both the front and backend application. This includes:
- How to write an asynchronous web server using rust's warp framework
- How to use wasm-bindgen, web-sys and js-sys crates to create a wasm npm module and publish it
- How to serve your single page app from the async web server
- How to create a react+redux front end, using WebRTC and WebSockets modules written in webassembly
- How to deploy your app to Openshift Online using docker
- How to set up unit and integration tests for the front and back end
- How to set up a CI pipeline between your deployment and your tests using travis-ci
What the app does
We will build the functionality of the app slowly in order to make it useful early on, but to build up functionality as we go.
- Chat application
- WebRTC for cam-to-cam video conferencing
- Video recognition
First, we will start with a relatively simple chat application. It will also cover things like setting up a database of users and saved chat history. It will also showcase how to do user authentication and authorization.
Next, we will write a webassembly module that interacts with WebRTC and WebSocket APIs. We will use this so that the webassembly can quickly and efficiently store data into tensor format that we can hand back to tensorflowjs.
Then, we will enhance the app so that it will do video as well as text based chatting. In this step, we will add a signaling service to the bacnd, so peers can find one another. Chats can either be saved locally or stored on the discovery server. This step will also show how to encrypt the streams for end to end encryption.
Lastly, we will build on the video streams enabled by the WebRTC to do image recognition. We will use this as a project to detect faces that are displayed and see if it is a known user.
Prerequisites
This chapter will cover how to scaffold and generate the initial wasm-pack code as well as a basic asynchronous web server but it assumed that the reader is already familiar with:
- basic rust
- basic html
- basic css
- basics of docker
The reader's rust skills should be at a basic level. One of the goals of the book is to explain some of the more tricky rust concepts (or at least tricky to the author). A basic understanding of rust ownership and lifetimes, and how closures work will be required. Understanding how Traits work, especially AsRef and From/Into will also be useful. The async/await portions will be described in detail however.
The reader should have a basic level of HTML5 understanding. This includes knowing what the DOM is and the basics of common tags. Most of the front end will be calling the web-sys bindings to the Web APIs, so it is useful but not required to know javascript. Because we are creating a single page app, knowledge of CSS will be useful, as the book will not spend a lot of time explaining much of the CSS content.
For deployment, we will be using Openshift Online. There is a free tier available and you can use this to deploy the application. In order to get the server to the cloud, we will need to create a container and therefore a docker image. This will not be advanced, but there will not be a lot of explanation of what the docker file does.
Caveats
The biggest caveat is the author is new to this himself. The decision to write the book was to help others so they do not have to learn the hard way like the author did. Also, the author is at a basic to intermediate level understanding of rust. There could very well be a better way to write the code and if so, please make a PR and contribute!
The second caveat was that this project made an opinionated stance on the technology used. First and foremost was the desire to use the new async/await syntax. This lead to several problems. For example, since async/await is still new, documentation is scarce.
Why not yew?
Seasoned developers might ask why yew was not used for this project. The simple answer here is that there was a desire to use wasm-bindgen, web-sys and js-sys crates to create the app.
The author has no working knowledge of yew, and it was considered initially. Afterall, it seems to tick off a lot of the right boxes:
- Built in virtual DOM
- Macros to generate JSX like code
- Concurrency
- Safe state management
However, yew is built on a crate called stdweb instead of wasm-bindgen, web-sys and js-sys. The main difference is that those libraries are created and maintained by the official Web Assembly Working Group. It will therefore be more up to date and have "official" support. It was also designed to be language agnostic and the bindings are auto-generated from the WebIDL schema.
Why not percy?
There is another framework that looked promising called percy. It also has a virtual DOM and macro generator to create some JSX-like code. Unlike yew, it is using wasm-bindgen and web-sys and js-sys crates. The problem with percy is that because of some of the macros, it required a nightly toolchain.
Although nightly is great for individual learning and experimentation, it's not the best for teaching others. The brittleness of nightly means that what may compile one day for one person may not compile for another person (or the same person!) on another day. It can also be hard sometimes to find a nightly build that allows all dependency crates to be built successfully.
Why not seed?
I discovered seed very recently, and while it seems to fit the bill for writing the entire application in rust, upon some consideration, I felt that it would be more beneficial to write the front end in react and typescript, and use wasm only for speed (or safety) sensitive areas of the code.
This also has the advantage of being able to slowly convert existing applications to use wasm, rather than have an all-in-one greenfield project created from scratch.
Local Setup
Now that you know what you will be building, let's get started setting up all the development dependencies. You'll need to have rust and npm set up as well as a few cargo tools
- rustup
- npm (recommend using nvm)
- cargo-generate
- cargo-edit
You can also checkout the github repository of khadga itself
Installing rustup
If you haven't already, install rustup by following the directions. If you already have rustup installed, make sure it's at the latest and greatest (at the time of writing, this is 1.40). To update your rustup, do
rustup self update
rustup update
Next, you need to set up the wasm32 target so that rustc can compile to the wasm32-unknown-unknown target triple
rustup target add wasm32-unknown-unknown
Other rustup goodies
While we are configuring rustup, we can install some other components as well
rustup component add llvm-tools-preview rustfmt clippy rls rust-analysis
C(++) toolchain
Some rust crates have native dependencies. For example the openssl crate will use and link to a native ssl lib on your system. Because of this, it's sometimes necessary to have a C(++) toolchain on your system as well.
It's beyond the scope of this book to show how to do this, since each operating system will do it a bit differently. For Windows, it's recommended to install one of the free Visual Studio C++ compilers. For linux debian based distributions, you can usually get away with something like this:
sudo apt install build-essential
For Fedora, you can do this:
sudo dnf groupinstall "Development Tools"
sudo dnf groupinstall "C Development Tools and Libraries"
Adding cargo tools
Although cargo is automatically installed by rustup, we are going to install some cargo additions.
cargo install cargo-generate cargo-edit
cargo-generate is a tool that will auto generate a template for you (and is used by wasm-pack) and cargo-edit is a nice little cargo command that lets you add a dependency to your Cargo.toml (think npm install).
Setting up vscode
We'll be using the Microsoft VS Code editor, since it has good support for rust and is relatively lightweight. Because we are using some bleeding edge crates, we'll also have to specify some additional configuration in the rust extension.
First, install vs code itself. Once you have code installed, we need to install the rust extension. You can either do this from the command line, or through VS Code itself.
code --install-extension rust-lang.rust
While we are installing extensions, let's install a couple others that will make our lives easier:
- crates: To make it easier to see what the latest (stable) crate version is at
- lldb debugger: So we can debug our code
- toml: so we can get syntax highlights and coloring for our toml files
code --install-extension bungcip.better-toml
code --install-extension vadimcn.vscode-lldb
code --install-extension serayuzgur.crates
Install npm (and nvm)
Since we are building a front end web app, we will be making use of some npm tools. It's highly recommended that you use the Node Version Manager (nvm) for this if you are on linux or MacOS. For windows users, you'll probably need to use chocolatey to install node (and npm).
linux and macos
For linux and macos users, you can follow the directions here to install nvm. Once you install nvm, you'll need to actually install a node version.
nvm install 13
nvm use 13
Windows
For windows users, if you don't have chocolatey already, install that. Then you can install node (and therefore npm) with:
choco install nodejs # make sure you run from an elevated command/powershell shell
Installing wasm-pack
For this project, we will be using wasm-pack which will generate a template for us, as well as set up a webpack config which will automatically compile our rust code to wasm.
You can install wasm-pack here.
Alternatively, you can install wasm-pack via cargo:
cargo install wasm-pack
Setting up the project
Now that we have all our dependencies out of the way, it's time to actually create our project. Unlike many rust projects you will see on tutorials, we are going to have a fairly advanced cargo setup. We will be using a feature of cargo called workspaces to work on the front and back end.
Creating your initial project
First, create a directory then create a Cargo.toml file in it:
mkdir -p ~/Projects/weblearn
cd ~/Projects/weblearn
touch Cargo.toml
The Cargo.toml file will need to be edited to look something like this:
[workspace]
members = [
"backend",
"frontend"
]
Generating the machine learning project
Since this is a (partially) isomorphic web projectrunning rust on the backend, and a combined webassembly/typescript front end (and a forthcoming rust IoT microcontroller sensor), we need a way to generate the the frontend project so that our rust code compiles to wasm, and all the glue clode to call to/from javascript functions can be done.
To set up the front end project you can go into your weblearn directory and run the following:
npm init rust-webpack mllib
This will generate a wasmpack style project that contains the code necessary for a combined rust and javascript project. Later, we will go into what files were generated, and how to build this workspace, but for now, you can browse this new frontend directory.
Using typescript
However, we are using rust, so why would we want to use javascript with it's dynamic types? We will enhance our project build code by allowing us to use typescript.
We will use a project called create-base-ts for this purpose. It will scaffold a project for use with typescript.
npm init base-ts frontend
npx tsc
The above commands will install typescript as a dev-dependency in our package.json file, and the
npx tsc
command will build our base project
We will tweak some of the configuration parameters later, once we build the project for the first time.
Generating the backend project
The backend will be a typical rust web server using actix-web, so we can just use cargo for this:
cd /path/to/weblearn
cargo new backend
Using kubernetes for testing and deployment
Ultimately we are creating a Google Cloud Product, and therefore we need to create a kubernetes project. As we develop the project and have something to actually test and deploy, we will go into more detail.
However, we can go over the actual build container. At the beginning of the chapter, we went over how to setup all your local development dependencies. However, we also need a reproducible way to build our project. While installing local tools is nice, what we really want to do in a production environment are many things:
- Automate the process of building all the artifacts our project needs
- As the artifacts are built, unit test them
- Once all the artifacts have been built, run integration tests
- If all the unit and integration tests pass, create a docker image
- Build your Kubernetes Objects (deployments, services, etc) locally
- Run minikube and apply all your config files to run your app
- Run end to end tests against the app
- If the tests pass
- Tag the build(s) of your image(s)
- push the image to a container repo like docker hub or GCR
- Tell GCP to use the latest images and reploy
Yes, that's a rather large set of things to do!! This is called CD or continuous deployment. This is slightly different from CI which stands for continuous integration. The main difference is that the latter will actually push out the end application/project to a public release, whereas the former is more about building and testing the artifacts, but not necessarily pushing the artifact to deployment.
Where does kubernetes fit in?
Why kubernetes?
We want a way to make reproducible builds. In the olden days, developers would have their local build environments, which often was some configuration of an IDE. When the developer was done with their feature or bug fix, they would click some button on their IDE to build the application, and maybe they might run some tests (yeah, this was before TDD, BDD, CI, etc). Assuming it looked good, said developer of yore would make a CVS or perforce commit and announce to all the coworkers that new source code was ready.
Very often, a coworker would checkout a copy of the new source code, and lo and behold, he couldn't even get the project to build! What trickery was this? Were gremlins sabotaging code in the ancient CVS revision control system? The fastidious engineer might have compared the source code to make sure it was indeed correct and identical to what the developer who originally built it successfully had on his or her system.
Well, the problem was often that one developer's configuration of his build tools was a little bit different. Or perhaps the build required configuration files to be present in a particular location, and in a particular state. It was also highly likely that the local dependencies on one developers workstation was different from another. For you youngsters who have only been programming a few years, you may have wondered where the terms DLL Hell, Classpath Hell, npm hell (pre yarn or package-lock.json days) etc all came from. Well, one of the symptoms of the above problems is when either your program has some kind of dependency conflict (package A requires package B of version 1.0, while package C requires package B at version 2.0), or it could also mean that the dependencies on your system are missing or the incorrect version.
Whole dependency management systems were written to try to tackle this problem. For example, pkg-config for C(++) shared libraries/object, ant/maven/gradle/ivy for Java dependency loading and classpath/modulepath configuration, etc etc.
When I first started learning docker, I first struggled with the concept until I realized it was just a kind of abstraction around a process which carried around a file system with it. But I also tended to think of it in terms of running applications. Afterall, one of the selling points was that an image was self-contained: it had all the dependencies and runtimes needed to run the application. Ever built a python, java, javascript, etc app at work or school, and wanted to have other people use it? Well, first, you had to tell them "oh yeah, before you run my application, you need to install python. How do you do that? Well, first you have to....". Now imagine some of your dependency libraries are tucked away in a private repository. Like maybe you have JFrog serving up a private npm, maven, pypi etc repo. Now you have to tell your users how to access the private repo.
Ughhh, what a pain.
So that's the first thing that containers solved. Of course, it's also why we are seeing a resurgence of "build to native" languages, like rust, go, nim, crystal and even haskell. To be honest, I always felt like docker containers were a bit of a hack to get around the popularity of interpreted languages (and yes, I am looking at you java, your bytecode still needs to be fed into a JVM to spit out the actual machine code). But containers still have their uses even with "build to native" languages.
There are 2 other use cases that come to mind:
- Creating a container that handles building a project
- Applications that require more than just a binary
Your build environment as a container
In order to build a rust project, what do you need?
- The rustc compiler
- Tools like cargo
You may think, ha! That's all I need. But is it? Some rust libraries link to native libraries. The most notable example of this is the openssl crate which leverages the system openssl.lib. Some crates will also attempt to link or build C code. In that case, you now need a C(++) compiler and set of tools like make, autoconf, pkg-config, ld, nm, ar, etc.
Or, what if you have a webassembly project? Now you need to install wasm-pack (well, technically you don't, but good luck setting it up manually). If you have a webassembly project, you will need npm installed too. Oops, to install npm, I need node.
Maybe you're getting the picture. There's a lot of stuff that you have to install and get right. We can automate this process by creating a Dockerfile that builds our...well, build environment.
Let's do that now by creating a Dockerfile, but we're going to put it somewhere special:
FROM fedora:31 as builder
RUN dnf update -y \
&& dnf upgrade -y \
&& dnf install -y curl openssl-devel \
&& dnf groupinstall -y "Development Tools" \
&& dnf groupinstall -y "C Development Tools and Libraries" \
&& dnf clean all -y \
&& mkdir -p /apps/vision
# TODO: create a khadga user and run as that user. We don't need to run as root
# Install rust tools
# Strictly speaking this isn't necessary for runtime. Adding these will create
# a container that lets us build khadga
RUN curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs > rustup.sh \
&& sh rustup.sh -y \
&& source ~/.cargo/env \
&& rustup update \
&& cargo install wasm-pack
# Install node tools
RUN curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.35.2/install.sh | bash
RUN source ~/.bashrc \
&& nvm install 13 \
&& nvm use 13 \
&& mkdir -p /apps/bundle \
&& mkdir -p /src
# TODO: either clone the remote repo, or copy the entire khadga project over.
# In the "build locally" version, we assume that we've built khadga on our dev machine
# but we could also build it in the docker container itself. Instead of doing a COPY
# we could do a MOUNT
COPY ./khadga /src/khadga
COPY ./vision /src/vision
COPY ./noesis /src/noesis
COPY ./build-docker.sh /src
WORKDIR /src
# TODO: If we need to publish a new noesis library, we need to
RUN source ~/.bashrc \
&& source ~/.cargo/env \
&& ./build-docker.sh \
&& ls -al
RUN rm -rf /src
FROM fedora:31
RUN mkdir -p /apps/vision
COPY --from=builder /apps/vision /apps/vision
WORKDIR /apps/vision
CMD [ "./khadga" ]
We want to put this Dockerfile in a dockers/build
folder. So your folder should look something like
this:
khadga/
- docker/
- build/
- Dockerfile
- khadga/
- noesis/
- vision/
We don't actually have any source code yet so we can't run this yet. But I'll show you how to run this.
cd /path/to/top-level/khadga
sudo docker build -f ./docker/builder/Dockerfile -t stoner/khadga .
So for example, if my khadga is in /home/stoner/Projects/khadga
, I would do:
cd /home/stoner/Projects/khadga
sudo docker build -f ./docker/builder/Dockerfile -t stoner/khadga .
Running this command will tell docker to use the special Dockerfile we just created and to tag it with stoner/khadga. If you don't use the -f option, docker will look in the current directory for a Dockerfile. Since we are going to have another Dockerfile in the root khadga directory, that's why we created and saved this Dockerfile somewhere special.
What does it do?
Basically the above Dockerfile will create a Fedora31 base image, update it with the latest goodies, and then install our tooling needed to build all the khadga subprojects (including the khadga backend, the noesis webrtc wasm library, and the vision front end).
Of note is that we use the FROM <base-image> as <phase-name>
. Usually, when you see the FROM
command in a Dockerfile, you don't see the as
part. We use this when we have a multiphase build.
Notice that further down almost at the bottom, we say again FROM fedora:31
. This means that at this
point docker can throw away the preceeding layers and start with the resulting image as a base
image. However, docker still "remembers" the old image. That's why you later see the command:
COPY --from=builder /apps/vision /apps/vision
The --from=builder
part says, "from our previous phase called builder, copy the /apps/vision
directory to our current image's /apps/vision directory. If we didn't use this phased approach, the
resulting image would have been about 2.6GB in size. But by using the phased approach, the size was
less than 1/10th the size at 203MB.
The other thing to note is the build-docker.sh
script that gets called. This is what actually
builds the subprojects. It will look like this:
ls -al
cd noesis
wasm-pack build
if [ "${PUBLISH}" = "true" ]; then
wasm-pack test --firefox
echo "Deploying noesis to npm"
npm login
wasm-pack publish
fi
cd ../vision
rm -rf node_modules
npm install
npm run clean
npm run build
npx jest
cd ../khadga
cargo build --release
# KHADGA_DEV=true cargo test
# Copy all the artifacts
cd ..
echo "Now in ${PWD}"
ls -al khadga
cp -r vision/dist /apps/vision/dist
cp -r khadga/config /apps/vision/config
cp -r khadga/target/release/khadga /apps/vision
As we build up our code, we will go into more detail on what's happening here.
Running kubernetes
Here, we will show how to set up minikube and use kubectl
Installing minikube
You can follow the directions to install minikube here
Using minikube
minikube start --vm-driver=kvm2
minikube status
Creating deployment files
TODO: Go over all the deployment files we need
Installing kubectl
You can follow the directions here to install kubectl
Setting up kubectl
To get kubectl to use the minikube k8s enviroment by creating a new namespace, run this command:
kubectl config set-context --namespace=localdev minikube
Kubectl commands
This command will expose an already running pod named khadga, and attach a LoadBalancer service with a name of khadga-service
kubectl expose deployment khadga --type=LoadBalancer --name=khadga-service
This command uses the kompose file, and will run the get command on everything in the kompose
kubectl get -k .
Will display all the services running in the kubernetes environment (for the default namespace)
kubectl get service
kubectl get deployment
kubectl get pod
kubectl get pod -o wide
Shows how to delete various kubernetes objects like Deployments or Services
kubectl delete deployment khadga
kubectl delete service khadga-service
Uses the kompose file to apply all the config files. Looks for a kompose file
kubectl apply -k .
Creates a new namespace. Namespaces isolate the kubernetes environment
kubectl create namespace localdev
kubectl config get-contexts
Setting up gcloud
You can follow the directions here to install the gcloud SDK.
Once the gcloud SDK is installed, you will also need to initialize it.
gcloud init
kubectl commands
Allow gcloud to use docker
gcloud auth configure-docker
To set up auth for logging in, run this command.
gcloud auth login
To set the project in google cloud to work with, run this command
gcloud config set project khadga-dev
To show a list of images in GCR run this command, where the argument to repository is the URL for the GCR repo (eg gcr.io, eu.gcr.io, etc), a slash, and then the name of the project
gcloud container images list --repository=gcr.io/khadga-dev
To show the tags of an image use this command
gcloud container images list-tags gcr.io/khadga-dev/khadga
docker-credential-gcloud configure-docker
To get credentials
gcloud container clusters get-credentials standard-cluster-1 --zone us-central1-a --project khadga-dev
General workflow for testing and deployment
- docker build your container images
- tag your images
- push the images to gcr.io
- Update the config file for the updated image tag
For testing
- create a localdev namespace and use it
- Create the secret so that localdev knows how to use it
- patch the serviceaccount to use the imagePullSecrets key
For the first step, you need to create a new namespace if you haven't already:
kubectl create namespace localdev
If you have run this step before, you don't need to again. You can go to step 3.
You can either append --namespace=localdev to all kubectl commands, or you can make it your default,
where the final argument is the name of your context
. To view your contexts:
kubectl config get-contexts
Then to set your namespace to the context you want:
kubectl config set-context --namespace=localdev minikube
Next, you can set up a new secret key by following there directions here
Then you need to patch your serviceaccount to use the key that you downloaded.
kubectl patch serviceaccount default -p '{"imagePullSecrets": [{"name": "gcr-json-key"}]}'
The main client interface
First, let's start off with a very basic webpage. Our end goal with this app is not to create something beautiful, but simply as a way to get data from some agent (a human, being, an IoT sensor, or another remote service for example) so that we can work on this data in real time.
Our initial goal is to provide a user interface for a human user (or some AI agent) to post messages similiar to slack. It's sort of half slack/gitter, half webchat in that the messages should be persistenet and editable, but they should also be seen in a public forum (or privately).
A second goal is to provide video camera access to allow for video using WebRTC.
The SPA running on the users browser has 2 initial services:
- Collect messages from an agent and do text classification on it
- Perform face recognition and eye tracking
General Layout
Before we get to actually coding up anything, let's think about what we want to show the user, and how we want the interface to be presented.
- Navbar
- Google Login
- Settings for app
- video/audio enumeration
- chat and appearance
- Chat messages window
- Public message window
- Message threads
- Private message window
- Logged in users
- Video (Draggable and Resizable div component)
State
What are some of the things we need to keep track of for our app?
- Is user logged in already? (ie, session is current and active?)
- What users have connected and logged in that we can chat with?
- Do we need to show modal for user to sign up? login?
- Max message/text limit for chat message windows before removing from DOM
- Is there a video chat session?
- SDP data for webrtc peer connection for both offer and reply
Navbar
TODO: Go over how the navbar is implemented in bulma.
Chat message window
TODO: Go over how to split up into a main public column, and another column for message threads
Public messages
TODO: Explain what the public messages are vs. threaded
Message threads
TODO: Allow replies to a message that follows that specific message
Private messages
TODO: How do we add new containers for private messages?
Logged in users
TODO Show logged in users
Video
React and state management
For this application we will be using react and redux for state management on the front end side of things at least in the beginning. Once we start getting into tensorflow, we will start using rust data structures to capture some of the data, however the state of the application itself will be handled with redux.
Why react
One could ask why we are using react here. There are other popular and not so popular front end frameworks out there including but not limited to angular, vue,
Why redux
Hooking a component into redux
- Add a new Action Creator with some new state to pass along
- Add a reducer that that does something with the action type and new state
- Add the new reducer to the combineReducers function
- Add a mapPropsToState on component and only pass the data needed (that the reducer takes)
- Add a mapDispatchToState if needed
- Used if component needs to change state that another component needs to react to
Navbar component
Google Authentication component
Mongo Database
We need a way to store information that the app either needs or collects. For example, we need to know information about a user that signs up, and what permissions he or she has. We also want a way to persist information about messages we receive so that we can operate on them later if needed.
In this chapter, we will go over setting up a mongodb database, and in later chapters we will revisit it to shore up security and create some new indexes to speed up searches. This chapter will also go over a docker set up to make it easier to set up our static backend files, and setup up the necessary networking glue to get our rust actix server and databse to talk to each other
Using Docker
In order to test our new code changes as well as deploy our final app, we need a way to deliver a usable artifact that we can either run tests against, or actually use in production.
Although rust is very nice in that it produces standalone binaries, our product here is actually a set of services. For example, khadga needs a database and we can't stuff a database inside our binary (well, we could, but where would you persist the data?). Other parts that probably aren't good to stuff inside the binary are configuration files. Many times you want to dynamically configure how an application runs based on a file that it can read. If we statically link in the file to the binary, it's stuck with that configuration. If you allow your app to read the config file at runtime, where and how do you bring along the configuration file(s)?
To solve these kinds of problems, we will use docker and docker-compose. If you are not that familiar with docker, that's fine. Here, I will explain what is going on, and why we are doing what we are doing.
Installing docker
Depending on your operating system, this can be a tricky affair, so I will direct you to the docs on the docker site.
Creating a Dockerfile
The Dockerfile will be our recipe to create a container that contains our backend server and the configuration files that it needs.
First, we create a file named Dockerfile in the root of our project. It will look like this:
FROM ubuntu:bionic
RUN apt update \
&& apt upgrade -y \
&& apt install -y curl \
&& mkdir -p /apps/vision/dist
# TODO: create a khadga user and run as that user. We don't need to run as root
# Copy the dist that was generated from wasm-pack and webpack to our working dir
# then, copy the executable to the vision directory. This is because the binary
# is serving files from ./dist
WORKDIR /apps/vision
COPY vision/dist ./dist
COPY khadga/config ./config
COPY target/debug/khadga .
CMD [ "./khadga" ]
Explanation
So what's this Dockerfile file doing? For those not familiar with docker, the Dockerfile is the default name for the file that the docker CLI will look for when running certain subcommands like build. It is a file that contains instructions for how to build a docker image step-by-step.
As a quick aside, do not confuse docker images, from docker containers. A container is really a
kind of fancy process with special restrictions. An image is like an executable binary. From one
binary, you can launch many processes. The docker image is the executable that a container is
spawned from. The command docker ps
will list running containers, while docker images
will list
docker images on your system.
Each line of code is actually creatig a new layer in a docker image (which is why you frequently see certains commands like RUN using && to concatenate commands to a single layer). This layering is also important to understand when you make changes to your Dockerfile. As each command in the Dockerfile is encountered, docker will create a layer which it caches so that it doesn't need to be done again. If you only ever add new steps at the bottom of the Dockerfile, that's fine. However, if you change a step at the beginning (for example), all steps after your new change must be executed again, since the layer system is immutable. This can increase your build times significantly.
Our base image
So our first step is a FROM
command, which basically specifies what base image to use. Here, we
are saying that the base image we will use is an ubuntu bionic release. Notice the use of the colon
here. To the left of the colon is the base image name (ubuntu in this case), and to the right of
the colon is a tag. The colon and tag name are optional, but if you do not use them, there is
typically a default tag (usually :latest
).
What this does is download from the docker hub repository an image named ubuntu, with the tag of bionic. This is our base image. From this base image, we will add more to it.
Adding new components to our base image
Next, we encounter the RUN
command. RUN
takes a shell command and executes it within a docker
daemon (which is actually executing our base image). If you look at the shell commands it is
running, you can see it is updating the operating system, adding curl and creating some directories.
Copying files from dev machine to docker image
A critical thing to understand in docker is the difference between our development machine and the docker image itself. We need to get the files from our dev machine onto the docker image. Another important thing to understand is the idea of a context which is like a build directory.
When you actually build your docker image, you specify a build context argument. For example:
sudo docker build -t <username></tag> .
The . at the end tells docker "use the current directory as the build context". The build context
is the point of view from your development machine. If you use your current directory as your build
context, any COPY
operations in the Dockerfile will assume that the source is from that build
context.
For the sake of the example, let's assume that you are currently in /path/to/my-project
. And that
you then run
The WORKDIR
command tells the docker daemon "use this directory as our working directory from the
docker image point of view". In the Dockerfile, we are using WORKDIR /app/vision
. Any command
that copies files for example, will use this directory as the destination directory by default.
So the next command COPY vision/dist ./dist
is basically saying "Copy the files from
${BuildContext}/vision/dist to ${WORKDIR}/dist". So, if you set your build context to "." during
the docker build
command, and that you are currently in the /path/to/my-project
directory, that
this will copy our project's vision/dist
directory to the docker image's /app/vision/dist
directory.
Hopefully the next COPY
commands are clear now. We also copy our local khadga/config on our dev
machine to the image's /app/vision/config directory. The last COPY
is interesting. Here, we
will copy the binary generated from cargo (in this case the debug version) to our docker image in
/app/vision. Eventually, we'll want to somehow dynamically set that based on whether we want to use
the docker container for testing or release it to production.
Executing our process
A docker container executes what's in the CMD
operation. The first arg is our command or
executable, and subsequent elements in the array are any arguments the executable needs.
Setting up mongodb with docker-compose
This chapter is about mongodb, but so far we haven't touched mongodb at all. Creating the Dockerfile was necessary however for our next piece, the docker-compose file, which will be explained in the next section
TODO: docker-compose
create mongodb container
TODO: schemas for data types
Yes, this is mongo, but schemas are not a bad thing.
TODO: setup initial db.rs code
Security
Authenticating a user
For khadga, we will be using the Google OAuth mechanism.
Setting things up
There are actually 2 parts to this:
- Setting up a google account with OAuth Credentials
- The front end client
Setting up google account for OAuth
The first thing we will need to do is to set up a google account. Go to
console.developers.google.com and select or create a project. Since the UI is always
changing, it's hard to say what to look for, but currently, in the left hand sidebar, there is a
link that says Credentials
.
If you click on Credentials
, a new layout appears that shows your current credentials for your
project:
- API Keys
- OAuth2 credentials
- Service Accounts
At the top, there is a button to + Create Credentials
. In the drop down that appears, select the
OAuth client ID option. In the next screen, you can select Web Option
. A new section appears and
you can optionally give it a name.
There will also be a field that specifies authorized javascript origins. For demo purposes, we can
put in http://localhost:7001
which is where our khadga backend, and therefore where our web page's
window.location
will be.
Setting up the front end client
The first thing we need to do is load the javascript. Unfortunately, google does not release an npm
package, so we need to load this in a <script>
tag in our index.html.
<head>
<meta charset="UTF-8" name="viewport" content="width=device-width, initial-scale=1">
<title>Grid test web app!</title>
<script defer src="https://use.fontawesome.com/releases/v5.3.1/js/all.js"></script>
<script src="https://apis.google.com/js/api.js"></script>
<link rel="stylesheet" href="./styles.css"><link>
</head>
Once we do this, we will have access to the gapi
object from our window.navigator. Since we are
using typescript, adding objects to the global window
object is a bit hacky. We will do a simple
approach which is not type-safe, and then improve on it later.
function latestName(conn, user) {
let baseName = user;
let re = new RegExp(`${baseName}-(\\d+)`);
let index = 0;
for (let name of conn) {
if (name === baseName) {
console.log(`${name} in list matches current of ${baseName}`)
let matched = name.match(re);
if (matched) {
index = parseInt(matched[1]) + 1;
console.log(matched);
console.log(`Got match, index is ${index}`);
baseName = baseName.replace(/\d+/, `${index}`);
} else {
index += 1;
baseName = `${baseName}-${index}`;
}
} else {
console.log(`${name} does not equal ${baseName}`)
}
console.log(`baseName is now ${baseName}`)
}
return baseName;
}
latestName(["sean", "sean-1", "sean-2"], "sean")
Letencrypt TLS certs
If you have an https server, you'll need a real TLS server. Creating self-signed certs is ok for testing purposes, but even then, unless you have good automation and CI/CD pipelines, there's a danger of accidentally deploying a server with a self-signed cert to production.
Here, we will talk about how to create a Letsencrypt TLS cert that you can use for the khadga backend
Certbot
TODO: Show how to use certbot and docker to autogenerate and autorenew a TLS cert that we can deploy
The backend
Even single page apps have to get downloaded to the user's browser somehow. This chapter will talk about how to set up a web server written in the warp framework for rust.
We will also go over the endpoints we'd like to establish, although in later chapters, we will flesh out some additional endpoints we would like to make.
Warp
TODO: Creating https in warp
Discuss nginx load balancer as an option
Authentication
We don't just want anyone to come in and use our service. We could allow guests to access some parts of the system, but not everything. The first step is having a user sign up and register themselves to the service.
Once a user is registered, we need a way to validate that the entity accessing the service, is who they actually say they are. This is authentication. This is not to be confused with authorization, which means that we have know you are John Doe, but we need to know what permissions John Doe has to our system.
Classic database authentication
We will be implementing a classic password + database security. While perhaps not the best system, we can later augment this.
TODO: Discuss how to send data from web page, to backend, and then to the mongodb.
2FA
TODO: using 2FA or TPM
Talk about stragies for stronger authN
Oauth
TODO: Talk about possibly implementing OAuth with google
Authorization
It's not good enough to authenticate the user is who he or she is, but we also need to restrict access to service to those who are authorized for those services.
TODO: Describe JWT tokens and how to use rust to generate them, and to have the khadga backend make use of them.
Backend Server
We need a server to supply a couple of services we need:
- Authentication service to provide JWT tokens
- Signaling service so peers can send WebRTC media to each other
- Websocket service for chatting
Noesis
Noesis in Greek means, "insight", "to understand" or "to see". I thought it was a good name for a webassembly module intended to grab data from different sources and as a helper for tensorflowjs.
What it does
Noesis will be a slowly growing webassembly module that will do the following:
- WebRTC handling, including getting MediaStreams and MediaDevices
- Tensor'izing data from websockets
- Tensor'izing data from MediaStreams
How it works
TODO: Explain how to use wasm-pack, wasm-bindgen, web-sys and js-sys