Add providers
Going from "Hello World" to a full-fledged application is simply a matter of identifying the capabilities your application needs, and then adding providers to deliver those capabilities. Providers in wasmCloud can invoke a function handler in your component in response to some external trigger (as with the first-party httpserver and messaging providers) and components can invoke providers in order to handle a request (as with our keyvalue providers). Our "Hello World" already uses the httpserver provider, so let's add more features to this application with functional code and more providers.
Adding functionality
- Rust
- TinyGo
- TypeScript
- Python
To perform the steps in this guide, you'll need to have a Rust toolchain installed locally and the wasm32 target installed:
rustup target add wasm32-wasi
Let's extend this application to do more than just say "Hello from Rust!" Using the path_with_query
method on the incoming request, we can check the request for a name provided in a query string, and then return a greeting with that name. If there isn't one or the path isn't in the format we expect, we'll default to saying "Hello, World!".
wit_bindgen::generate!({
generate_all
});
use exports::wasi::http::incoming_handler::Guest;
use wasi::http::types::*;
struct HttpServer;
impl Guest for HttpServer {
fn handle(_request: IncomingRequest, response_out: ResponseOutparam) {
fn handle(request: IncomingRequest, response_out: ResponseOutparam) {
let response = OutgoingResponse::new(Fields::new());
response.set_status_code(200).unwrap();
let response_body = response.body().unwrap();
let name = match request
.path_with_query()
.unwrap()
.split("=")
.collect::<Vec<&str>>()[..]
{
// query string is "/?name=<name>" e.g. localhost:8080?name=Bob
["/?name", name] => name.to_string(),
// query string is anything else or empty e.g. localhost:8080
_ => "World".to_string(),
};
response_body
.write()
.unwrap()
.blocking_write_and_flush(b"Hello from Rust!\n")
.blocking_write_and_flush(format!("Hello, {}!\n", name).as_bytes())
.unwrap();
OutgoingBody::finish(response_body, None).expect("failed to finish response body");
ResponseOutparam::set(response_out, Ok(response));
}
}
export!(HttpServer);
Let's extend this application to do more than just say "Hello from Go!" Using the PathWithQuery
method on the incoming request, we can check the request for a name provided in a query string, and then return a greeting with that name. If there isn't one or the path isn't in the format we expect, we'll default to saying "Hello, World!". To make this code more readable, we'll add a helper function to extract the name from the path.
package main
import (
"fmt"
"strings"
http "github.com/wasmcloud/wasmcloud/examples/golang/components/http-hello-world/gen"
)
// Helper type aliases to make code more readable
type HttpRequest = http.ExportsWasiHttp0_2_0_IncomingHandlerIncomingRequest
type HttpResponseWriter = http.ExportsWasiHttp0_2_0_IncomingHandlerResponseOutparam
type HttpOutgoingResponse = http.WasiHttp0_2_0_TypesOutgoingResponse
type HttpError = http.WasiHttp0_2_0_TypesErrorCode
type HttpServer struct{}
func init() {
httpserver := HttpServer{}
// Set the incoming handler struct to HttpServer
http.SetExportsWasiHttp0_2_0_IncomingHandler(httpserver)
}
func (h HttpServer) Handle(request HttpRequest, responseWriter HttpResponseWriter) {
// Construct HttpResponse to send back
headers := http.NewFields()
httpResponse := http.NewOutgoingResponse(headers)
httpResponse.SetStatusCode(200)
body := httpResponse.Body().Unwrap()
bodyWrite := body.Write().Unwrap()
bodyWrite.BlockingWriteAndFlush([]uint8("Hello from Go!\n")).Unwrap()
name := getNameFromPath(request.PathWithQuery().Unwrap())
bodyWrite.BlockingWriteAndFlush([]uint8(fmt.Sprintf("Hello %s!\n", name))).Unwrap()
// Send HTTP response
okResponse := http.Ok[HttpOutgoingResponse, HttpError](httpResponse)
bodyWrite.Drop()
http.StaticOutgoingBodyFinish(body, http.None[http.WasiHttp0_2_0_TypesTrailers]())
http.StaticResponseOutparamSet(responseWriter, okResponse)
}
func getNameFromPath(path string) string {
parts := strings.Split(path, "=")
if len(parts) == 2 {
return parts[1]
}
return "World"
}
//go:generate wit-bindgen tiny-go wit --out-dir=gen --gofmt
func main() {}
To perform the steps in this guide, you'll need you'll need to install npm and run npm install
in the project directory. We use npm to install dependencies and compile TypeScript code to WebAssembly.
Let's extend this application to do more than just say "hello." Using the pathWithQuery
method on the incoming request, we can check the request for a name provided in a query string, and then return a greeting with that name. If there isn't one or the path isn't in the format we expect, we'll default to saying "hello world!". To make this code more readable, we'll add a helper function to extract the name from the path.
import { IncomingRequest, ResponseOutparam, OutgoingBody, OutgoingResponse, Fields } from "wasi:http/types@0.2.0";
// Implementation of wasi-http incoming-handler
//
// NOTE: To understand the types involved, take a look at wit/deps/http/types.wit
function handle(req: IncomingRequest, resp: ResponseOutparam) {
// Start building an outgoing response
const outgoingResponse = new OutgoingResponse(new Fields());
// Access the outgoing response body
let outgoingBody = outgoingResponse.body();
{
// Create a stream for the response body
let outputStream = outgoingBody.write();
const name = getNameFromPath(req.pathWithQuery() || "")
// Write "hello [name]" to the response stream
outputStream.blockingWriteAndFlush(
new Uint8Array(new TextEncoder().encode("hello from Typescript"))
new Uint8Array(new TextEncoder().encode(`Hello ${name}!\n`))
);
// @ts-ignore: This is required in order to dispose the stream before we return
outputStream[Symbol.dispose]();
}
// Set the status code for the response
outgoingResponse.setStatusCode(200);
// Finish the response body
OutgoingBody.finish(outgoingBody, undefined);
// Set the created response
ResponseOutparam.set(resp, { tag: "ok", val: outgoingResponse });
}
function getNameFromPath(path: string): string {
const parts = path.split("=");
if (parts.length == 2) {
return parts[1];
}
return "world";
}
export const incomingHandler = {
handle,
};
To perform the steps in this guide, you'll need you'll need to install Python 3.10 or later, pip, and componentize-py v0.12 or later to compile Python code to WebAssembly.
pip install componentize-py
Let's extend this application to do more than just say "hello." Using the path_with_query
method on the incoming request, we can check the request for a name provided in a query string, and then return a greeting with that name. If there isn't one or the path isn't in the format we expect, we'll default to saying "hello world!". We'll also add a helper function to keep our code readable.
If you're using an IDE or would like to look at the imports, you can generate the bindings using componentize-py
to get IDE suggestions and autofills.
componentize-py bindings .
class IncomingHandler(exports.IncomingHandler):
def handle(self, _: IncomingRequest, response_out: ResponseOutparam):
def handle(self, request: IncomingRequest, response_out: ResponseOutparam):
# Construct the HTTP response to send back
outgoingResponse = OutgoingResponse(Fields.from_list([]))
# Set the status code to OK
outgoingResponse.set_status_code(200)
outgoingBody = outgoingResponse.body()
# Write our Hello message to the response body
outgoingBody.write().blocking_write_and_flush(bytes("Hello from Python!\n", "utf-8"))
name = get_name_from_path(request.path_with_query())
outgoingBody.write().blocking_write_and_flush(bytes("Hello {}!\n".format(name), "utf-8"))
OutgoingBody.finish(outgoingBody, None)
# Set and send the HTTP response
ResponseOutparam.set(response_out, Ok(outgoingResponse))
def get_name_from_path(path: str) -> str:
parts = path.split("=")
if len(parts) == 2:
return parts[-1]
else:
return "World"
After changing the code, you can use wash
to build the local component:
wash build
Deploying your component
Now that you've made an update to your component, you can use wash to stop the previous version. You can stop the component based on its unique identifier, which Wadm automatically assigned based on the name of your application and the name of your component. Once stopped, Wadm will take care of starting the new local copy from the updated file.
- Rust
- TinyGo
- TypeScript
- Python
wash stop component rust_hello_world-http_component
wash stop component tinygo_hello_world-http_component
wash stop component typescript_hello_world-http_component
wash stop component python_hello_world-http_component
Whenever you make a change to your component that you want to deploy, be sure to run wash build
to recompile and generate a new .wasm file.
> curl localhost:8080
Hello, World!
> curl 'localhost:8080?name=Bob'
Hello, Bob!
Adding persistent storage
To further enhance our application, let's add persistent storage to keep a record of each person that this application greeted. We'll use the key-value store provider for this, and just like HTTP server, you won't need to pick a library or a specific vendor implementation yet. You'll just need to add the appropriate interface to your component, and then you can pick a provider at runtime.
In your template, we already included the wasi:keyvalue
interface for interacting with a key value store. We can also use the wasi:logging
interface to log the name of each person we greet. Before you can use the functionality of those interfaces, you'll need to add a few imports to your wit/world.wit
file:
package wasmcloud:hello;
world hello {
import wasi:keyvalue/atomics@0.2.0-draft;
import wasi:keyvalue/store@0.2.0-draft;
import wasi:logging/logging;
export wasi:http/incoming-handler@0.2.0;
}
- Rust
- TinyGo
- TypeScript
- Python
Let's use the atomic increment function to keep track of how many times we've greeted each person.
let name = match request
.path_with_query()
.unwrap()
.split("=")
.collect::<Vec<&str>>()[..]
{
// query string is "/?name=<name>" e.g. localhost:8080?name=Bob
["/?name", name] => name.to_string(),
// query string is anything else or empty e.g. localhost:8080
_ => "World".to_string(),
};
wasi::logging::logging::log(
wasi::logging::logging::Level::Info,
"",
&format!("Greeting {name}"),
);
let bucket =
wasi::keyvalue::store::open("").expect("failed to open empty bucket");
let count = wasi::keyvalue::atomics::increment(&bucket, &name, 1)
.expect("failed to increment count");
response_body
.write()
.unwrap()
.blocking_write_and_flush(format!("Hello x{count}, {name}!\n").as_bytes())
.unwrap();
Let's use the atomic increment function to keep track of how many times we've greeted each person.
func (h HttpServer) Handle(request HttpRequest, responseWriter HttpResponseWriter) {
// Construct HttpResponse to send back
headers := http.NewFields()
httpResponse := http.NewOutgoingResponse(headers)
httpResponse.SetStatusCode(200)
body := httpResponse.Body().Unwrap()
bodyWrite := body.Write().Unwrap()
name := getNameFromPath(request.PathWithQuery().Unwrap())
http.WasiLoggingLoggingLog(http.WasiLoggingLoggingLevelInfo(), "", fmt.Sprintf("Greeting %s", name))
bucket := http.WasiKeyvalue0_2_0_draft_StoreOpen("").Unwrap()
count := http.WasiKeyvalue0_2_0_draft_AtomicsIncrement(bucket, name, 1).Unwrap()
bodyWrite.BlockingWriteAndFlush([]uint8(fmt.Sprintf("Hello x%d, %s!\n", count, name))).Unwrap()
bodyWrite.BlockingWriteAndFlush([]uint8("Hello from Go!\n")).Unwrap()
// Send HTTP response
okResponse := http.Ok[HttpOutgoingResponse, HttpError](httpResponse)
bodyWrite.Drop()
http.StaticOutgoingBodyFinish(body, http.None[http.WasiHttp0_2_0_TypesTrailers]())
http.StaticResponseOutparamSet(responseWriter, okResponse)
}
Let's use the atomic increment function to keep track of how many times we've greeted each person.
At the time of writing, JCO does not generate types for wasi:logging
or wasi:keyvalue
. This is
a known issue and will be resolved in a future release. For now, you can tell the TypeScript
compiler to ignore the missing types by adding //@ts-expect-error
before each import statement.
Simply including the import statement will allow the host to provider the functionality at runtime.
import {
IncomingRequest,
ResponseOutparam,
OutgoingBody,
OutgoingResponse,
Fields,
} from 'wasi:http/types@0.2.0';
//@ts-expect-error -- these types aren't currently generated by JCO
import { log } from 'wasi:logging/logging';
//@ts-expect-error -- these types aren't currently generated by JCO
import { increment } from 'wasi:keyvalue/atomics@0.2.0-draft';
// @ts-expect-error -- these types aren't currently generated by JCO
import { open } from 'wasi:keyvalue/store@0.2.0-draft';
// Implementation of wasi-http incoming-handler
//
// NOTE: To understand the types involved, take a look at wit/deps/http/types.wit
function handle(req: IncomingRequest, resp: ResponseOutparam) {
// Start building an outgoing response
const outgoingResponse = new OutgoingResponse(new Fields());
// Access the outgoing response body
let outgoingBody = outgoingResponse.body();
// Write to the response stream
const name = getNameFromPath(req.pathWithQuery() || '');
log('info', '', `Greeting ${name}`);
// Increment the bucket's count
const bucket = open('');
const count = increment(bucket, name, 1);
{
// Create a stream for the response body
let outputStream = outgoingBody.write();
// Write hello world to the response stream
outputStream.blockingWriteAndFlush(
new Uint8Array(new TextEncoder().encode(`Hello x${count}, ${name}!\n`)),
);
// @ts-ignore: This is required in order to dispose the stream before we return
outputStream[Symbol.dispose]();
}
// Set the status code for the response
outgoingResponse.setStatusCode(200);
// Finish the response body
OutgoingBody.finish(outgoingBody, undefined);
// Set the created response
ResponseOutparam.set(resp, { tag: 'ok', val: outgoingResponse });
}
Let's use the atomic increment function to keep track of how many times we've greeted each person.
from hello import exports
from hello.types import Ok
from hello.imports.types import (
IncomingRequest, ResponseOutparam,
OutgoingResponse, Fields, OutgoingBody
)
from hello.imports.atomics import increment
from hello.imports.store import open
from hello.imports.logging import (log, Level)
class IncomingHandler(exports.IncomingHandler):
def handle(self, request: IncomingRequest, response_out: ResponseOutparam):
# Construct the HTTP response to send back
outgoingResponse = OutgoingResponse(Fields.from_list([]))
# Set the status code to OK
outgoingResponse.set_status_code(200)
outgoingBody = outgoingResponse.body()
# Write our Hello message to the response body
name = get_name_from_path(request.path_with_query())
log(Level.INFO, "", "Greeting {}".format(name))
bucket = open("")
count = increment(bucket, name, 1)
outgoingBody.write().blocking_write_and_flush(bytes("Hello x{}, {}!\n".format(count, name), "utf-8"))
OutgoingBody.finish(outgoingBody, None)
# Set and send the HTTP response
ResponseOutparam.set(response_out, Ok(outgoingResponse))
We've made changes, so run wash build
again to compile the Wasm component.
wash build
Deploying a key-value store provider
Our component is prepared to use a key-value store, and now that we've built it we're ready to choose an implementation. A great option for local development and testing is the Redis provider, and will only require you to have redis-server
or Docker installed.
- Local Redis Server
- Docker
Install and launch the local redis server in the background
redis-server &
Launch a Redis container in the background
docker run -d --name redis -p 6379:6379 redis
The Redis provider will mediate a connection to the Redis server, which is running external to wasmCloud. We can modify our wadm.yaml
to include the Redis provider and configure a link for our component. Since we're nearing the end of this tutorial, we'll provide the full manifest here:
apiVersion: core.oam.dev/v1beta1
kind: Application
metadata:
name: hello-world
annotations:
description: 'HTTP hello world demo, using the WebAssembly Component Model and WebAssembly Interfaces Types (WIT)'
spec:
components:
- name: http-component
type: component
properties:
# Your manifest will point to the path of the built component, you can also
# start published components from OCI registries
image: ghcr.io/wasmcloud/components/http-hello-world-rust:0.1.0
traits:
- type: spreadscaler
properties:
replicas: 1
# The new key-value link configuration
- type: link
properties:
target: kvredis
namespace: wasi
package: keyvalue
interfaces: [atomics, store]
target_config:
- name: redis-url
properties:
url: redis://127.0.0.1:6379
# The new capability provider
- name: kvredis
type: capability
properties:
image: ghcr.io/wasmcloud/keyvalue-redis:0.27.0
- name: httpserver
type: capability
properties:
image: ghcr.io/wasmcloud/http-server:0.22.0
traits:
- type: link
properties:
target: http-component
namespace: wasi
package: http
interfaces: [incoming-handler]
source_config:
- name: default-http
properties:
address: 127.0.0.1:8080
You'll notice that the kvredis and httpserver providers are pulled from images hosted on GitHub Packages. The wasmCloud ecosystem uses the OCI image specification to package components and providers—these component and provider images are not container images, but conform to OCI standards and may be stored on any OCI-compatible registry.
Deploy the latest version of our application including the key-value provider. Then, again, we can test our new functionality.
> wash app deploy wadm.yaml
> curl 'localhost:8080?name=Bob'
Hello x1, Bob!
> curl 'localhost:8080?name=Bob'
Hello x2, Bob!
> curl 'localhost:8080?name=Alice'
Hello x1, Alice!
Moving on
In this tutorial, you added a few more features and persistent storage to a simple microservice. You also got to see the process of developing with interfaces, where the code you write is purely functional, doesn't require you to pick a library or vendor upfront, and allows you to change your application separately from its non-functional requirements. You can continue to build on this application by adding more features, or you can explore additional first-party providers in the wasmCloud repo (see any provider-* directory) to get an idea of what is possible with wasmCloud.
The next page demonstrates scaling via Wadm manifest and gives a high level overview of why this application that you've built already eliminates complexity and pain that developers often face when building applications for the cloud.