Adding Capabilities
Going from "Hello World" to a full-fledged application requires just identifying what capabilities your application needs, and then adding them to your actor. Capabilities in wasmCloud can invoke a function handler in your actor in response to some external trigger (like HTTP Server and Messaging) and actors can invoke capabilities as a part of handling a request (like key-value store and logging). "Hello World" already has the HTTP Server capability, so let's add more features to this application with functional code and more capabilities.
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!({
world: "hello",
exports: {
"wasi:http/incoming-handler": HttpServer,
},
});
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));
}
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/actors/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)
httpResponse.Body().Unwrap().Write().Unwrap().BlockingWriteAndFlush([]uint8("Hello from Go!\n")).Unwrap()
name := getNameFromPath(request.PathWithQuery().Unwrap())
httpResponse.Body().Unwrap().Write().Unwrap().BlockingWriteAndFlush([]uint8(fmt.Sprintf("Hello %s!\n", name))).Unwrap()
// Send HTTP response
okResponse := http.Ok[HttpOutgoingResponse, HttpError](httpResponse)
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,
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();
// Write "hello [name]" to the response stream
const name = getNameFromPath(req.pathWithQuery() || "")
outputStream.blockingWriteAndFlush(
new Uint8Array(new TextEncoder().encode("hello from Typescript"))
new Uint8Array(new TextEncoder().encode(`hello ${name}!`))
);
// Set the status code for the response
outgoingResponse.setStatusCode(200);
// 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 actor:
wash build
Deploying your Actor
Now that you've made an update to your actor, you can use wash to stop the previous version. You can supply either the name of the actor or the full 56 character signing key to the wash stop actor
command. wadm will take care of starting the new local copy from the updated file.
wash stop actor http-hello-world
Whenever you make a change to your actor 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 capability 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 capability to your actor, and then you can pick a capability 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/hello.wit
file:
package wasmcloud:hello;
world hello {
import wasi:keyvalue/atomic@0.1.0;
import wasi:keyvalue/eventual@0.1.0;
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::types::Bucket::open_bucket("").expect("failed to open empty bucket");
let count = wasi::keyvalue::atomic::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)
name := getNameFromPath(request.PathWithQuery().Unwrap())
http.WasiLoggingLoggingLog(http.WasiLoggingLoggingLevelInfo(), "", fmt.Sprintf("Greeting %s", name))
bucket := http.StaticBucketOpenBucket("").Unwrap()
count := http.WasiKeyvalue0_1_0_AtomicIncrement(bucket, name, 1).Unwrap()
httpResponse.Body().Unwrap().Write().Unwrap().BlockingWriteAndFlush([]uint8(fmt.Sprintf("Hello x%d, %s!\n", count, name))).Unwrap()
httpResponse.Body().Unwrap().Write().Unwrap().BlockingWriteAndFlush([]uint8("Hello from Go!\n")).Unwrap()
// Send HTTP response
okResponse := http.Ok[HttpOutgoingResponse, HttpError](httpResponse)
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, 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/atomic@0.1.0';
//@ts-expect-error -- these types aren't currently generated by JCO
import { Bucket } from 'wasi:keyvalue/types@0.1.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();
// Write to the response stream
const name = getNameFromPath(req.pathWithQuery() || '');
log('info', '', `Greeting ${name}`);
// Increment the bucket's count
const bucket = Bucket.openBucket('');
const count = increment(bucket, name, 1);
outputStream.blockingWriteAndFlush(
new Uint8Array(new TextEncoder().encode(`Hello x${count}, ${name}!\n`)),
);
// Set the status code for the response
outgoingResponse.setStatusCode(200);
// 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
OutgoingResponse, Fields, OutgoingBody, Bucket
)
from hello.imports.atomic import increment
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 = Bucket.open_bucket("")
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))
Deploying a Key-Value Store Provider
Our actor 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
We can modify our wadm.yaml
to include the Redis provider and configure a link for our actor. 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: http-hello-world
annotations:
version: v0.0.3 # Increment the version number
description: 'HTTP hello world demo, using the WebAssembly Component Model and WebAssembly Interfaces Types (WIT)'
experimental: 'true'
spec:
components:
- name: http-hello-world
type: actor
properties:
image: file://./build/http_hello_world_s.wasm
traits:
- type: spreadscaler
properties:
replicas: 50
- type: linkdef
properties:
target: httpserver
values:
address: 0.0.0.0:8080
# The new key-value link configuration
- type: linkdef
properties:
target: keyvalue
values:
address: redis://127.0.0.1:6379
- name: httpserver
type: capability
properties:
image: wasmcloud.azurecr.io/httpserver:0.19.1
contract: wasmcloud:httpserver
# The new capability provider
- name: keyvalue
type: capability
properties:
image: wasmcloud.azurecr.io/kvredis:0.22.0
contract: wasmcloud:keyvalue
For the last step, we can deploy the v0.0.3
version of our application. 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 capabilities and providers in wasmCloud Capability Providers to get an idea of what is possible with wasmCloud.
The next page 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.