diff --git a/README.md b/README.md index a65f2fc..50076f9 100644 --- a/README.md +++ b/README.md @@ -20,6 +20,8 @@ These samples provide a starting point for how to integrate different services u Icon indicates Sample is compatible with [Docker Dev Environments](https://docs.docker.com/desktop/dev-environments/) in Docker Desktop version 4.10 or later. + Icon indicates Sample is compatible with [Docker+Wasm](https://docs.docker.com/desktop/wasm/). + - [`ASP.NET / MS-SQL`](aspnet-mssql) - Sample ASP.NET core application with MS SQL server database. - [`Elasticsearch / Logstash / Kibana`](elasticsearch-logstash-kibana) - Sample Elasticsearch, Logstash, and Kibana stack. @@ -49,6 +51,8 @@ application with a Rust backend and a Postgres database. - [`Spring / PostgreSQL`](spring-postgres) - Sample Java application with Spring framework and a Postgres database. +- [`WasmEdge / MySQL / Nginx`](wasmedge-mysql-nginx) - Sample Wasm-based web application with a static HTML frontend, using a MySQL (MariaDB) database. The frontend connects to a Wasm microservice written in Rust, that runs using the WasmEdge runtime. +- [`WasmEdge / Kafka / MySQL`](wasmedge-kafka-mysql) - Sample Wasm-based microservice that subscribes to a Kafka (Redpanda) queue topic, and transforms and saves any incoming message into a MySQL (MariaDB) database. ## Single service samples diff --git a/icon_wasm.svg b/icon_wasm.svg new file mode 100644 index 0000000..001cb62 --- /dev/null +++ b/icon_wasm.svg @@ -0,0 +1,13 @@ + + + + + + + + diff --git a/wasmedge-kafka-mysql/.docker/docker-compose.yml b/wasmedge-kafka-mysql/.docker/docker-compose.yml new file mode 100644 index 0000000..6163108 --- /dev/null +++ b/wasmedge-kafka-mysql/.docker/docker-compose.yml @@ -0,0 +1,36 @@ +services: + redpanda: + image: docker.redpanda.com/vectorized/redpanda:v22.2.2 + command: + - redpanda start + - --smp 1 + - --overprovisioned + - --node-id 0 + - --kafka-addr PLAINTEXT://0.0.0.0:29092,OUTSIDE://0.0.0.0:9092 + - --advertise-kafka-addr PLAINTEXT://redpanda:29092,OUTSIDE://redpanda:9092 + - --pandaproxy-addr 0.0.0.0:8082 + - --advertise-pandaproxy-addr localhost:8082 + ports: + - 8081:8081 + - 8082:8082 + - 9092:9092 + - 9644:9644 + - 29092:29092 + volumes: + - ./kafka:/app + etl: + image: etl-kafka + platform: wasi/wasm + build: + context: etl + environment: + DATABASE_URL: mysql://root:whalehello@db:3306/mysql + KAFKA_URL: kafka://redpanda:9092/order + RUST_BACKTRACE: full + RUST_LOG: info + restart: unless-stopped + runtime: io.containerd.wasmedge.v1 + db: + image: mariadb:10.9 + environment: + MYSQL_ROOT_PASSWORD: whalehello diff --git a/wasmedge-kafka-mysql/README.md b/wasmedge-kafka-mysql/README.md new file mode 100644 index 0000000..1efd5d7 --- /dev/null +++ b/wasmedge-kafka-mysql/README.md @@ -0,0 +1,117 @@ +# Compose sample application + +![Compatible with Docker+Wasm](../icon_wasm.svg) + +This sample demonstrates a WebAssembly (Wasm) microservice written in Rust. It subscribes to a Kafka queue topic on a Redpanda server, and then transforms and saves each message into a MySQL (MariaDB) database table. The microservice is compiled into Wasm and runs in the WasmEdge runtime, which is a secure and lightweight alternative to natively compiled Rust apps in Linux containers. + +## Use with Docker Development Environments + +You will need a version of Docker Desktop or Docker CLI with Wasm support. + +* [Install Docker Desktop + Wasm (Beta)](https://docs.docker.com/desktop/wasm/) +* [Install Docker CLI + Wasm](https://github.com/chris-crone/wasm-day-na-22/tree/main/server) + +## WasmEdge server with Redpanda and MySQL database + +Project structure: + +``` +. ++-- compose.yml +|-- etl + |-- Dockerfile + |-- Cargo.toml + +-- src + |-- main.rs +|-- kafka + |-- order.json +|-- db + |-- db-password.txt +``` + +The [compose.yml](compose.yml) is as follows. + +```yaml +services: + redpanda: + image: docker.redpanda.com/vectorized/redpanda:v22.2.2 + command: + - redpanda start + - --smp 1 + - --overprovisioned + - --node-id 0 + - --kafka-addr PLAINTEXT://0.0.0.0:29092,OUTSIDE://0.0.0.0:9092 + - --advertise-kafka-addr PLAINTEXT://redpanda:29092,OUTSIDE://redpanda:9092 + - --pandaproxy-addr 0.0.0.0:8082 + - --advertise-pandaproxy-addr localhost:8082 + ports: + - 8081:8081 + - 8082:8082 + - 9092:9092 + - 9644:9644 + - 29092:29092 + volumes: + - ./kafka:/app + + etl: + image: etl-kafka + build: + context: etl + platforms: + - wasi/wasm32 + environment: + DATABASE_URL: mysql://root:whalehello@db:3306/mysql + KAFKA_URL: kafka://redpanda:9092/order + RUST_BACKTRACE: full + RUST_LOG: info + restart: unless-stopped + runtime: io.containerd.wasmedge.v1 + + db: + image: mariadb:10.9 + environment: + MYSQL_ROOT_PASSWORD: whalehello +``` + +The compose file defines an application with three services `redpanda`, `etl` and `db`. The `redpanda` service is a Kafka-compatible messaging server that produces messages in a queue topic. The `etl` service, in the WasmEdge container that subscribes to the queue topic and receives incoming messages. Each incoming message is parsed and stored in the `db` MySQL (MariaDB) database server. + +## Deploy with docker compose + +```bash +$ docker compose up -d +... + ⠿ Network wasmedge-kafka-mysql_default Created 0.1s + ⠿ Container wasmedge-kafka-mysql-redpanda-1 Created 0.3s + ⠿ Container wasmedge-kafka-mysql-etl-1 Created 0.3s + ⠿ Container wasmedge-kafka-mysql-db-1 Created 0.3s +``` + +## Expected result + +```bash +$ docker compose ps +NAME COMMAND SERVICE STATUS PORTS +wasmedge-kafka-mysql-db-1 "docker-entrypoint.s…" db running 3306/tcp +wasmedge-kafka-mysql-etl-1 "kafka.wasm" etl running +wasmedge-kafka-mysql-redpanda-1 "/entrypoint.sh 'red…" redpanda running 0.0.0.0:8081-8082->8081-8082/tcp, :::8081-8082->8081-8082/tcp, 0.0.0.0:9092->9092/tcp, :::9092->9092/tcp, 0.0.0.0:9644->9644/tcp, :::9644->9644/tcp, 0.0.0.0:29092->29092/tcp, :::29092->29092/tcp +``` + +After the application starts, +log into the Redpanda container and send a message to the queue topic `order` as follows. + +```bash +$ docker compose exec redpanda /bin/bash +redpanda@1add2615774b:/$ cd /app +redpanda@1add2615774b:/app$ cat order.json | rpk topic produce order +Produced to partition 0 at offset 0 with timestamp 1667922788523. +``` + +To see the data in the database container, you can use the following commands. + +```bash +$ docker compose exec db /bin/bash +root@c97c472db02e:/# mysql -u root -pwhalehello mysql +mysql> select * from orders; +... ... +``` + diff --git a/wasmedge-kafka-mysql/compose.yml b/wasmedge-kafka-mysql/compose.yml new file mode 100644 index 0000000..6163108 --- /dev/null +++ b/wasmedge-kafka-mysql/compose.yml @@ -0,0 +1,36 @@ +services: + redpanda: + image: docker.redpanda.com/vectorized/redpanda:v22.2.2 + command: + - redpanda start + - --smp 1 + - --overprovisioned + - --node-id 0 + - --kafka-addr PLAINTEXT://0.0.0.0:29092,OUTSIDE://0.0.0.0:9092 + - --advertise-kafka-addr PLAINTEXT://redpanda:29092,OUTSIDE://redpanda:9092 + - --pandaproxy-addr 0.0.0.0:8082 + - --advertise-pandaproxy-addr localhost:8082 + ports: + - 8081:8081 + - 8082:8082 + - 9092:9092 + - 9644:9644 + - 29092:29092 + volumes: + - ./kafka:/app + etl: + image: etl-kafka + platform: wasi/wasm + build: + context: etl + environment: + DATABASE_URL: mysql://root:whalehello@db:3306/mysql + KAFKA_URL: kafka://redpanda:9092/order + RUST_BACKTRACE: full + RUST_LOG: info + restart: unless-stopped + runtime: io.containerd.wasmedge.v1 + db: + image: mariadb:10.9 + environment: + MYSQL_ROOT_PASSWORD: whalehello diff --git a/wasmedge-kafka-mysql/db/db-password.txt b/wasmedge-kafka-mysql/db/db-password.txt new file mode 100644 index 0000000..21eee51 --- /dev/null +++ b/wasmedge-kafka-mysql/db/db-password.txt @@ -0,0 +1 @@ +whalehello diff --git a/wasmedge-kafka-mysql/etl/Cargo.toml b/wasmedge-kafka-mysql/etl/Cargo.toml new file mode 100644 index 0000000..657c1db --- /dev/null +++ b/wasmedge-kafka-mysql/etl/Cargo.toml @@ -0,0 +1,17 @@ +[package] +name = "kafka" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +anyhow = "1.0.65" +mega_etl = {git = "https://github.com/second-state/MEGA.git"} +tokio_wasi = {version = '1.21', features = ["rt", "macros"]} +env_logger = "0.9" +log = "0.4" +serde = { version = "1.0", features = ["derive"] } +serde_json = "1.0" +http_req_wasi = "0.10" +lazy_static = "1.4.0" diff --git a/wasmedge-kafka-mysql/etl/Dockerfile b/wasmedge-kafka-mysql/etl/Dockerfile new file mode 100644 index 0000000..62df729 --- /dev/null +++ b/wasmedge-kafka-mysql/etl/Dockerfile @@ -0,0 +1,27 @@ +# syntax=docker/dockerfile:1 +FROM --platform=$BUILDPLATFORM rust:1.64 AS buildbase +RUN <) -> TransformerResult> { + let s = std::str::from_utf8(&inbound_data) + .map_err(|e| TransformerError::Custom(e.to_string()))?; + let order: Order = serde_json::from_str(String::from(s).as_str()) + .map_err(|e| TransformerError::Custom(e.to_string()))?; + log::info!("{:?}", &order); + let mut ret = vec![]; + let sql_string = format!( + r"INSERT INTO orders VALUES ({:?}, {:?}, {:?}, {:?}, {:?}, {:?}, {:?}, CURRENT_TIMESTAMP);", + order.order_id, + order.product_id, + order.quantity, + order.amount, + order.shipping, + order.tax, + order.shipping_address, + ); + dbg!(sql_string.clone()); + ret.push(sql_string); + Ok(ret) + } + + async fn init() -> TransformerResult { + Ok(String::from( + r"CREATE TABLE IF NOT EXISTS orders (order_id INT, product_id INT, quantity INT, amount FLOAT, shipping FLOAT, tax FLOAT, shipping_address VARCHAR(50), date_registered TIMESTAMP DEFAULT CURRENT_TIMESTAMP);", + )) + } +} + +#[tokio::main(flavor = "current_thread")] +async fn main() -> anyhow::Result<()> { + env_logger::init(); + + // can use builder later + let database_uri = std::env::var("DATABASE_URL")?; + let kafka_uri = std::env::var("KAFKA_URL")?; + let mut pipe = Pipe::new(database_uri, kafka_uri).await; + + // This is async because this calls the async transform() function in Order + pipe.start::().await?; + Ok(()) +} diff --git a/wasmedge-kafka-mysql/kafka/order.json b/wasmedge-kafka-mysql/kafka/order.json new file mode 100644 index 0000000..aeb1a38 --- /dev/null +++ b/wasmedge-kafka-mysql/kafka/order.json @@ -0,0 +1 @@ +{"order_id": 1,"product_id": 12,"quantity": 2,"amount": 56.0,"shipping": 15.0,"tax": 2.0,"shipping_address": "Mataderos 2312"} diff --git a/wasmedge-mysql-nginx/.docker/docker-compose.yml b/wasmedge-mysql-nginx/.docker/docker-compose.yml new file mode 100644 index 0000000..86559a8 --- /dev/null +++ b/wasmedge-mysql-nginx/.docker/docker-compose.yml @@ -0,0 +1,25 @@ +services: + frontend: + image: nginx:alpine + ports: + - 8090:80 + volumes: + - ./frontend:/usr/share/nginx/html + + backend: + image: demo-microservice + platform: wasi/wasm + build: + context: backend/ + ports: + - 8080:8080 + environment: + DATABASE_URL: mysql://root:whalehello@db:3306/mysql + RUST_BACKTRACE: full + restart: unless-stopped + runtime: io.containerd.wasmedge.v1 + + db: + image: mariadb:10.9 + environment: + MYSQL_ROOT_PASSWORD: whalehello diff --git a/wasmedge-mysql-nginx/README.md b/wasmedge-mysql-nginx/README.md new file mode 100644 index 0000000..7396c6a --- /dev/null +++ b/wasmedge-mysql-nginx/README.md @@ -0,0 +1,125 @@ +# Compose sample application + +![Compatible with Docker+Wasm](../icon_wasm.svg) + +This sample demonstrates a web application with a WebAssembly (Wasm) microservice, written in Rust. The Wasm microservice is an HTTP API connected to a MySQL (MariaDB) database. The API is invoked via from JavaScript in a web interface serving static HTML. The microservice is compiled into WebAssembly (Wasm) and runs in the WasmEdge Runtime, a secure and lightweight alternative to natively compiled Rust apps in Linux containers. Checkout [this article](https://blog.logrocket.com/rust-microservices-server-side-webassembly/) or [this video](https://www.youtube.com/watch?v=VSqMPFr7SEs) to learn how the Rust code in this microservice works. + +## Use with Docker Development Environments + +You will need a version of Docker Desktop or Docker CLI with Wasm support. + +* [Install Docker Desktop + Wasm (Beta)](https://docs.docker.com/desktop/wasm/) +* [Install Docker CLI + Wasm](https://github.com/chris-crone/wasm-day-na-22/tree/main/server) + +## WasmEdge server with Nginx proxy and MySQL database + +Project structure: + +``` +. ++-- compose.yml +|-- backend + +-- Dockerfile + |-- Cargo.toml + |-- src + +-- main.rs +|-- frontend + +-- index.html + |-- js + +-- app.js +|-- db + +-- orders.json + |-- update_order.json +``` + +The [compose.yml](compose.yml) file: + +```yaml +services: + frontend: + image: nginx:alpine + ports: + - 8090:80 + volumes: + - ./frontend:/usr/share/nginx/html + + backend: + image: demo-microservice + build: + context: backend/ + platforms: + - wasi/wasm32 + ports: + - 8080:8080 + environment: + DATABASE_URL: mysql://root:whalehello@db:3306/mysql + RUST_BACKTRACE: full + restart: unless-stopped + runtime: io.containerd.wasmedge.v1 + + db: + image: mariadb:10.9 + environment: + MYSQL_ROOT_PASSWORD: whalehello +``` + +The compose file defines an application with three services `frontend`, `backend` and `db`. The `frontend` is a simple Nginx server that hosts static web pages that access the `backend` web service, in the WasmEdge container, via HTTP port 8080. When deploying the application, docker compose maps port 8090 of the `frontend` service container to port 8090 of the host as specified in the file. Make sure that ports 8090 and 8080 on the host are not already being used. + +## Deploy with docker compose + +```bash +$ docker compose up -d +... + ⠿ Network wasmedge-mysql-nginx_default Created + ⠿ Container wasmedge-mysql-nginx-db-1 Created + ⠿ Container wasmedge-mysql-nginx-frontend-1 Created + ⠿ Container wasmedge-mysql-nginx-backend-1 Created +``` + +## Expected result + +```bash +$ docker compose ps +NAME COMMAND SERVICE STATUS PORTS +wasmedge-mysql-nginx-backend-1 "order_demo_service.…" backend running 0.0.0.0:8080->8080/tcp, :::8080->8080/tcp +wasmedge-mysql-nginx-db-1 "docker-entrypoint.s…" db running 3306/tcp +wasmedge-mysql-nginx-frontend-1 "/docker-entrypoint.…" frontend running 0.0.0.0:8090->80/tcp, :::8090->80/tcp +``` + +After the application starts, go to `http://localhost:8090` in your web browser to display the web frontend. + +### Using the API with `curl` + +As an alternative to the web frontend, you can use `curl` to interact with the WasmEdge API directly (the `backend` service). + +When the WasmEdge web service receives a GET request to the `/init` endpoint, it would initialize the database with the `orders` table. + +```bash +curl http://localhost:8080/init +``` + +When the WasmEdge web service receives a POST request to the `/create_order` endpoint, it extracts the JSON data from the POST body and inserts an `Order` record into the database table. +To insert multiple records, use the `/create_orders` endpoint and POST a JSON array of `Order` objects: + +```bash +curl http://localhost:8080/create_orders -X POST -d @db/orders.json +``` + +When the WasmEdge web service receives a GET request to the `/orders` endpoint, it gets all rows from the `orders` table and return the result set in a JSON array in the HTTP response. + +```bash +curl http://localhost:8080/orders +``` + +When the WasmEdge web service receives a POST request to the `/update_order` endpoint, it extracts the JSON data from the POST body and update the `Order` record in the database table that matches the `order_id` in the input data. + +```bash +curl http://localhost:8080/update_order -X POST -d @db/update_order.json +``` + +When the WasmEdge web service receives a GET request to the `/delete_order` endpoint, it deletes the row in the `orders` table that matches the `id` GET parameter. + +```bash +curl http://localhost:8080/delete_order?id=2 +``` + diff --git a/wasmedge-mysql-nginx/backend/Cargo.toml b/wasmedge-mysql-nginx/backend/Cargo.toml new file mode 100644 index 0000000..406cf3f --- /dev/null +++ b/wasmedge-mysql-nginx/backend/Cargo.toml @@ -0,0 +1,13 @@ +[package] +name = "order_demo_service" +version = "0.1.0" +edition = "2021" + +[dependencies] +anyhow = "1.0" +serde_json = "1.0" +serde = { version = "1.0", features = ["derive"] } +url = "2.3" +mysql_async_wasi = "0.30" +hyper_wasi = { version = "0.15", features = ["full"] } +tokio_wasi = { version = "1", features = ["io-util", "fs", "net", "time", "rt", "macros"] } diff --git a/wasmedge-mysql-nginx/backend/Dockerfile b/wasmedge-mysql-nginx/backend/Dockerfile new file mode 100644 index 0000000..17652e9 --- /dev/null +++ b/wasmedge-mysql-nginx/backend/Dockerfile @@ -0,0 +1,29 @@ +# syntax=docker/dockerfile:1 + +FROM --platform=$BUILDPLATFORM rust:1.64 AS buildbase +WORKDIR /src +RUN < String { + if let Ok(url) = std::env::var("DATABASE_URL") { + let opts = Opts::from_url(&url).expect("DATABASE_URL invalid"); + if opts + .db_name() + .expect("a database name is required") + .is_empty() + { + panic!("database name is empty"); + } + url + } else { + "mysql://root:pass@127.0.0.1:3306/mysql".into() + } +} + +#[derive(Serialize, Deserialize, Debug)] +struct Order { + order_id: i32, + product_id: i32, + quantity: i32, + amount: f32, + shipping: f32, + tax: f32, + shipping_address: String, +} + +impl Order { + fn new( + order_id: i32, + product_id: i32, + quantity: i32, + amount: f32, + shipping: f32, + tax: f32, + shipping_address: String, + ) -> Self { + Self { + order_id, + product_id, + quantity, + amount, + shipping, + tax, + shipping_address, + } + } +} + +async fn handle_request(req: Request, pool: Pool) -> Result, anyhow::Error> { + match (req.method(), req.uri().path()) { + (&Method::GET, "/") => Ok(Response::new(Body::from( + "The valid endpoints are /init /create_order /create_orders /update_order /orders /delete_order", + ))), + + // Simply echo the body back to the client. + (&Method::POST, "/echo") => Ok(Response::new(req.into_body())), + + // CORS OPTIONS + (&Method::OPTIONS, "/init") => Ok(response_build(&String::from(""))), + (&Method::OPTIONS, "/create_order") => Ok(response_build(&String::from(""))), + (&Method::OPTIONS, "/create_orders") => Ok(response_build(&String::from(""))), + (&Method::OPTIONS, "/update_order") => Ok(response_build(&String::from(""))), + (&Method::OPTIONS, "/delete_order") => Ok(response_build(&String::from(""))), + (&Method::OPTIONS, "/orders") => Ok(response_build(&String::from(""))), + + (&Method::GET, "/init") => { + let mut conn = pool.get_conn().await.unwrap(); + "DROP TABLE IF EXISTS orders;".ignore(&mut conn).await?; + "CREATE TABLE orders (order_id INT, product_id INT, quantity INT, amount FLOAT, shipping FLOAT, tax FLOAT, shipping_address VARCHAR(20));".ignore(&mut conn).await?; + drop(conn); + Ok(response_build("{\"status\":true}")) + } + + (&Method::POST, "/create_order") => { + let mut conn = pool.get_conn().await.unwrap(); + + let byte_stream = hyper::body::to_bytes(req).await?; + let order: Order = serde_json::from_slice(&byte_stream).unwrap(); + + "INSERT INTO orders (order_id, product_id, quantity, amount, shipping, tax, shipping_address) VALUES (:order_id, :product_id, :quantity, :amount, :shipping, :tax, :shipping_address)" + .with(params! { + "order_id" => order.order_id, + "product_id" => order.product_id, + "quantity" => order.quantity, + "amount" => order.amount, + "shipping" => order.shipping, + "tax" => order.tax, + "shipping_address" => &order.shipping_address, + }) + .ignore(&mut conn) + .await?; + + drop(conn); + Ok(response_build("{\"status\":true}")) + } + + (&Method::POST, "/create_orders") => { + let mut conn = pool.get_conn().await.unwrap(); + + let byte_stream = hyper::body::to_bytes(req).await?; + let orders: Vec = serde_json::from_slice(&byte_stream).unwrap(); + + "INSERT INTO orders (order_id, product_id, quantity, amount, shipping, tax, shipping_address) VALUES (:order_id, :product_id, :quantity, :amount, :shipping, :tax, :shipping_address)" + .with(orders.iter().map(|order| { + params! { + "order_id" => order.order_id, + "product_id" => order.product_id, + "quantity" => order.quantity, + "amount" => order.amount, + "shipping" => order.shipping, + "tax" => order.tax, + "shipping_address" => &order.shipping_address, + } + })) + .batch(&mut conn) + .await?; + + drop(conn); + Ok(response_build("{\"status\":true}")) + } + + (&Method::POST, "/update_order") => { + let mut conn = pool.get_conn().await.unwrap(); + + let byte_stream = hyper::body::to_bytes(req).await?; + let order: Order = serde_json::from_slice(&byte_stream).unwrap(); + + "UPDATE orders SET product_id=:product_id, quantity=:quantity, amount=:amount, shipping=:shipping, tax=:tax, shipping_address=:shipping_address WHERE order_id=:order_id" + .with(params! { + "product_id" => order.product_id, + "quantity" => order.quantity, + "amount" => order.amount, + "shipping" => order.shipping, + "tax" => order.tax, + "shipping_address" => &order.shipping_address, + "order_id" => order.order_id, + }) + .ignore(&mut conn) + .await?; + + drop(conn); + Ok(response_build("{\"status\":true}")) + } + + (&Method::GET, "/orders") => { + let mut conn = pool.get_conn().await.unwrap(); + + let orders = "SELECT * FROM orders" + .with(()) + .map(&mut conn, |(order_id, product_id, quantity, amount, shipping, tax, shipping_address)| { + Order::new( + order_id, + product_id, + quantity, + amount, + shipping, + tax, + shipping_address, + )}, + ).await?; + + drop(conn); + Ok(response_build(serde_json::to_string(&orders)?.as_str())) + } + + (&Method::GET, "/delete_order") => { + let mut conn = pool.get_conn().await.unwrap(); + + let params: HashMap = req.uri().query().map(|v| { + url::form_urlencoded::parse(v.as_bytes()).into_owned().collect() + }).unwrap_or_else(HashMap::new); + let order_id = params.get("id"); + + "DELETE FROM orders WHERE order_id=:order_id" + .with(params! { "order_id" => order_id, }) + .ignore(&mut conn) + .await?; + + drop(conn); + Ok(response_build("{\"status\":true}")) + } + + // Return the 404 Not Found for other routes. + _ => { + let mut not_found = Response::default(); + *not_found.status_mut() = StatusCode::NOT_FOUND; + Ok(not_found) + } + } +} + +// CORS headers +fn response_build(body: &str) -> Response { + Response::builder() + .header("Access-Control-Allow-Origin", "*") + .header("Access-Control-Allow-Methods", "GET, POST, OPTIONS") + .header("Access-Control-Allow-Headers", "api,Keep-Alive,User-Agent,Content-Type") + .body(Body::from(body.to_owned())) + .unwrap() +} + +#[tokio::main(flavor = "current_thread")] +async fn main() -> Result<(), Box> { + let opts = Opts::from_url(&*get_url()).unwrap(); + let builder = OptsBuilder::from_opts(opts); + // The connection pool will have a min of 5 and max of 10 connections. + let constraints = PoolConstraints::new(5, 10).unwrap(); + let pool_opts = PoolOpts::default().with_constraints(constraints); + let pool = Pool::new(builder.pool_opts(pool_opts)); + + let addr = SocketAddr::from(([0, 0, 0, 0], 8080)); + let make_svc = make_service_fn(|_| { + let pool = pool.clone(); + async move { + Ok::<_, Infallible>(service_fn(move |req| { + let pool = pool.clone(); + handle_request(req, pool) + })) + } + }); + let server = Server::bind(&addr).serve(make_svc); + if let Err(e) = server.await { + eprintln!("server error: {}", e); + } + Ok(()) +} diff --git a/wasmedge-mysql-nginx/compose.yml b/wasmedge-mysql-nginx/compose.yml new file mode 100644 index 0000000..86559a8 --- /dev/null +++ b/wasmedge-mysql-nginx/compose.yml @@ -0,0 +1,25 @@ +services: + frontend: + image: nginx:alpine + ports: + - 8090:80 + volumes: + - ./frontend:/usr/share/nginx/html + + backend: + image: demo-microservice + platform: wasi/wasm + build: + context: backend/ + ports: + - 8080:8080 + environment: + DATABASE_URL: mysql://root:whalehello@db:3306/mysql + RUST_BACKTRACE: full + restart: unless-stopped + runtime: io.containerd.wasmedge.v1 + + db: + image: mariadb:10.9 + environment: + MYSQL_ROOT_PASSWORD: whalehello diff --git a/wasmedge-mysql-nginx/db/orders.json b/wasmedge-mysql-nginx/db/orders.json new file mode 100644 index 0000000..9f2d21e --- /dev/null +++ b/wasmedge-mysql-nginx/db/orders.json @@ -0,0 +1,47 @@ +[ + { + "order_id": 1, + "product_id": 12, + "quantity": 2, + "amount": 56.0, + "shipping": 15.0, + "tax": 2.0, + "shipping_address": "Mataderos 2312" + }, + { + "order_id": 2, + "product_id": 15, + "quantity": 3, + "amount": 256.0, + "shipping": 30.0, + "tax": 16.0, + "shipping_address": "1234 NW Bobcat" + }, + { + "order_id": 3, + "product_id": 11, + "quantity": 5, + "amount": 536.0, + "shipping": 50.0, + "tax": 24.0, + "shipping_address": "20 Havelock" + }, + { + "order_id": 4, + "product_id": 8, + "quantity": 8, + "amount": 126.0, + "shipping": 20.0, + "tax": 12.0, + "shipping_address": "224 Pandan Loop" + }, + { + "order_id": 5, + "product_id": 24, + "quantity": 1, + "amount": 46.0, + "shipping": 10.0, + "tax": 2.0, + "shipping_address": "No.10 Jalan Besar" + } +] diff --git a/wasmedge-mysql-nginx/db/update_order.json b/wasmedge-mysql-nginx/db/update_order.json new file mode 100644 index 0000000..4f39358 --- /dev/null +++ b/wasmedge-mysql-nginx/db/update_order.json @@ -0,0 +1,9 @@ +{ + "order_id": 3, + "product_id": 12, + "quantity": 2, + "amount": 56.0, + "shipping": 15.0, + "tax": 2.0, + "shipping_address": "123 Main Street" +} diff --git a/wasmedge-mysql-nginx/frontend/index.html b/wasmedge-mysql-nginx/frontend/index.html new file mode 100644 index 0000000..d07c384 --- /dev/null +++ b/wasmedge-mysql-nginx/frontend/index.html @@ -0,0 +1,102 @@ + + + + Demo App + + + + + + + Loading... + + + + Welcome to the Demo! + This application is served using nginx for the website, Wasm for the backend, and MariaDB for the database. + + + There are currently no orders to display! + + + + + + Id + Product Id + Quantity + Amount + Shipping + Tax + Address + + + + + + + + + + + + + + + Add an order + + + + + + + Order Id + + The ID of the order + + + Product Id + + The ID of the product + + + Quantity + + How many of the product? + + + Amount + + The total amount + + + Tax + + The total amount of tax + + + Shipping Amount + + The total amount for shipping + + + Shipping Address + + Where to send the order + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/wasmedge-mysql-nginx/frontend/js/app.js b/wasmedge-mysql-nginx/frontend/js/app.js new file mode 100644 index 0000000..abaeb4b --- /dev/null +++ b/wasmedge-mysql-nginx/frontend/js/app.js @@ -0,0 +1,135 @@ +(function() { + let orders = null; + const appLoadingEle = document.getElementById("app-loading-display"); + const orderWrapperEle = document.getElementById("order-display"); + const orderEmptyTextEle = document.getElementById("order-empty-text"); + const orderTableEle = document.getElementById("order-table"); + const orderTableBodyEle = document.querySelector("#order-table tbody"); + const addOrderEle = document.getElementById("add-order-wrapper"); + const addOrderForm = document.getElementById("add-order-form"); + + const orderIdField = document.getElementById("order-id"); + const productIdField = document.getElementById("product-id"); + const quantityField = document.getElementById("quantity"); + const amountField = document.getElementById("amount"); + const taxField = document.getElementById("tax"); + const shippingField = document.getElementById("shippingAmount"); + const shippingAddressField = document.getElementById("shippingAddress"); + + function fetchOrders() { + fetch("http://localhost:8080/orders") + .then(r => r.json()) + .then(r => orders = r) + .then(renderOrders) + .catch((e) => { + init(); + }); + } + + function init() { + fetch("http://localhost:8080/init") + .then(() => fetchOrders()) + .catch((e) => displayError(e)); + } + + function renderOrders() { + appLoadingEle.classList.add("d-none"); + orderWrapperEle.classList.remove("d-none"); + addOrderEle.classList.remove("d-none"); + + if (orders.length === 0) { + orderEmptyTextEle.classList.remove("d-none"); + orderTableEle.classList.add("d-none"); + return; + } + + orderEmptyTextEle.classList.add("d-none"); + orderTableEle.classList.remove("d-none"); + + while (orderTableBodyEle.firstChild) { + orderTableBodyEle.removeChild(orderTableBodyEle.firstChild); + } + + orders.forEach((order) => { + const orderId = order.order_id; + + const row = document.createElement("tr"); + + row.appendChild(createCell(order.order_id)); + row.appendChild(createCell(order.product_id)); + row.appendChild(createCell(order.quantity)); + row.appendChild(createCell(order.amount)); + row.appendChild(createCell(order.shipping)); + row.appendChild(createCell(order.tax)); + row.appendChild(createCell(order.shipping_address)); + + const actionCell = document.createElement("td"); + + const deleteButton = document.createElement("button"); + deleteButton.classList.add(...["btn","btn-sm","btn-danger"]); + deleteButton.innerText = "Delete"; + + deleteButton.addEventListener("click", (e) => { + e.preventDefault(); + deleteOrder(orderId); + }); + + actionCell.appendChild(deleteButton); + + row.appendChild(actionCell); + + orderTableBodyEle.appendChild(row); + }); + } + + function createCell(contents) { + const cell = document.createElement("td"); + cell.innerText = contents; + return cell; + } + + function deleteOrder(orderId) { + fetch(`http://localhost:8080/delete_order?id=${orderId}`) + .then(() => fetchOrders()); + } + + function displayError(err) { + alert("Error:" + err); + } + + function onAddFormSubmit(e) { + e.preventDefault(); + + const data = { + order_id : parseFloat(orderIdField.value), + product_id : parseFloat(productIdField.value), + quantity : parseFloat(quantityField.value), + amount : parseFloat(amountField.value), + shipping : parseFloat(shippingField.value), + tax : parseFloat(taxField.value), + shipping_address : shippingAddressField.value, + }; + + fetch("http://localhost:8080/create_order", { + method: "POST", + body: JSON.stringify(data), + headers: { "Content-type": "application/json" }, + }).then(() => fetchOrders()) + .then(() => resetAddOrderForm()); + + alert("Order added"); + } + + function resetAddOrderForm() { + orderIdField.value = ""; + productIdField.value = ""; + quantityField.value = ""; + amountField.value = ""; + shippingField.value = ""; + taxField.value = ""; + shippingAddressField.value = ""; + } + + fetchOrders(); + addOrderForm.addEventListener("submit", onAddFormSubmit); +})(); \ No newline at end of file
This application is served using nginx for the website, Wasm for the backend, and MariaDB for the database.