diff --git a/README.md b/README.md index 32d5651266..3fabab8897 100644 --- a/README.md +++ b/README.md @@ -38,6 +38,7 @@ Project Layout * [`design`](design): Design documentation. See the [design/README.md](design/README.md) for details about building / viewing. * `codegen-server`: Whitelabel Smithy server code generation * `codegen-server-test`: Smithy protocol test generation & integration tests for Smithy server whitelabel code +* `examples`: A collection of server implementation examples Testing ------- diff --git a/design/src/server/anatomy.md b/design/src/server/anatomy.md index 394a057166..51f7b68f2d 100644 --- a/design/src/server/anatomy.md +++ b/design/src/server/anatomy.md @@ -648,7 +648,7 @@ stateDiagram-v2 Op1 --> Op2 : Plugin#colon;#colon;map ``` -An example `Plugin` implementation can be found in [aws-smithy-http-server/examples/pokemon-service/src/plugin.rs](https://github.com/awslabs/smithy-rs/blob/main/rust-runtime/aws-smithy-http-server/examples/pokemon-service/src/plugin.rs). +An example `Plugin` implementation can be found in [/examples/pokemon-service/src/plugin.rs](https://github.com/awslabs/smithy-rs/blob/main/examples/pokemon-service/src/plugin.rs). The service builder API requires plugins to be specified upfront - they must be passed as an argument to `builder_with_plugins` and cannot be modified afterwards. This constraint is in place to ensure that all handlers are upgraded using the same set of plugins. diff --git a/design/src/server/instrumentation.md b/design/src/server/instrumentation.md index 31eef1c38b..d946e51b0e 100644 --- a/design/src/server/instrumentation.md +++ b/design/src/server/instrumentation.md @@ -69,7 +69,7 @@ let app = PokemonService::builder_with_plugins(plugins) ### Example -The Pokémon service example, located at `rust-runtime/aws-smithy-http-server/examples/pokemon-service`, sets up a `tracing` `Subscriber` as follows: +The Pokémon service example, located at `/examples/pokemon-service`, sets up a `tracing` `Subscriber` as follows: ```rust /// Setup `tracing::subscriber` to read the log level from RUST_LOG environment variable. diff --git a/rust-runtime/aws-smithy-http-server/examples/.gitignore b/examples/.gitignore similarity index 82% rename from rust-runtime/aws-smithy-http-server/examples/.gitignore rename to examples/.gitignore index 8349ce53e7..aaf1fa215c 100644 --- a/rust-runtime/aws-smithy-http-server/examples/.gitignore +++ b/examples/.gitignore @@ -1,2 +1,3 @@ pokemon-service-client/ pokemon-service-server-sdk/ +Cargo.lock diff --git a/rust-runtime/aws-smithy-http-server/examples/BENCHMARKS.md b/examples/BENCHMARKS.md similarity index 72% rename from rust-runtime/aws-smithy-http-server/examples/BENCHMARKS.md rename to examples/BENCHMARKS.md index a5a3393066..bbc33c7b97 100644 --- a/rust-runtime/aws-smithy-http-server/examples/BENCHMARKS.md +++ b/examples/BENCHMARKS.md @@ -5,11 +5,12 @@ using [wrk](https://github.com/wg/wrk). -* [2022-03-04](#2022-03-04) - * [c6i.8xlarge](#c6i.8xlarge) - * [Full result](#full-result) - * [c6g.8xlarge](#c6g.8xlarge) - * [Full result](#full-result) +- [Smithy Rust Server SDK benchmarks](#smithy-rust-server-sdk-benchmarks) + - [2022-03-04](#2022-03-04) + - [c6i.8xlarge](#c6i8xlarge) + - [Full result](#full-result) + - [c6g.8xlarge](#c6g8xlarge) + - [Full result](#full-result-1) @@ -20,19 +21,19 @@ returning an empty output and can be used to stress test the framework overhead. ### c6i.8xlarge -* 32 cores Intel(R) Xeon(R) Platinum 8375C CPU @ 2.90GHz -* 64 Gb memory -* Benchmark: - - Duration: 10 minutes - - Connections: 1024 - - Threads: 16 -* Result: - - Request/sec: 1_608_742 - * RSS[^1] memory: 72200 bytes +- 32 cores Intel(R) Xeon(R) Platinum 8375C CPU @ 2.90GHz +- 64 Gb memory +- Benchmark: + - Duration: 10 minutes + - Connections: 1024 + - Threads: 16 +- Result: + - Request/sec: 1_608_742 + - RSS[^1] memory: 72200 bytes #### Full result -``` +```text ❯❯❯ wrk -t16 -c1024 -d10m --latency http://localhost:13734/empty-operation Running 10m test @ http://localhost:13734/empty-operation 16 threads and 1024 connections @@ -52,20 +53,20 @@ Transfer/sec: 167.23MB ### c6g.8xlarge -* 32 cores Amazon Graviton 2 @ 2.50GHz -* 64 Gb memory -* Benchmark: - - Duration: 10 minutes - - Connections: 1024 - - Threads: 16 -* Result: - - Request/sec: 1_379_942 - - RSS[^1] memory: 70264 bytes +- 32 cores Amazon Graviton 2 @ 2.50GHz +- 64 Gb memory +- Benchmark: + - Duration: 10 minutes + - Connections: 1024 + - Threads: 16 +- Result: + - Request/sec: 1_379_942 + - RSS[^1] memory: 70264 bytes #### Full result -``` +```text ❯❯❯ wrk -t16 -c1024 -d10m --latency http://localhost:13734/empty-operation Running 10m test @ http://localhost:13734/empty-operation 16 threads and 1024 connections diff --git a/rust-runtime/aws-smithy-http-server/examples/Cargo.toml b/examples/Cargo.toml similarity index 65% rename from rust-runtime/aws-smithy-http-server/examples/Cargo.toml rename to examples/Cargo.toml index e96e074b32..33c374bbcb 100644 --- a/rust-runtime/aws-smithy-http-server/examples/Cargo.toml +++ b/examples/Cargo.toml @@ -1,9 +1,12 @@ # Without this configuration, the workspace will be read from `rust-runtime`, causing the build to fail. [workspace] members = [ + "pokemon-service-common", "pokemon-service", + "pokemon-service-tls", + "pokemon-service-lambda", "pokemon-service-server-sdk", - "pokemon-service-client", + "pokemon-service-client" ] [profile.release] diff --git a/rust-runtime/aws-smithy-http-server/examples/Makefile b/examples/Makefile similarity index 100% rename from rust-runtime/aws-smithy-http-server/examples/Makefile rename to examples/Makefile diff --git a/examples/README.md b/examples/README.md new file mode 100644 index 0000000000..7f08738455 --- /dev/null +++ b/examples/README.md @@ -0,0 +1,49 @@ +# Smithy Rust Server SDK examples + +This folder contains an example services showcasing the service framework capabilities and to run benchmarks. + +- `/pokemon-service`, a HTTP server implementation demonstrating [middleware](https://awslabs.github.io/smithy-rs/design/server/middleware.html) +and [extractors](https://awslabs.github.io/smithy-rs/design/server/from_parts.html). +- `/pokemon-service-tls`, a minimal HTTPS server implementation. +- `/pokemon-service-lambda`, a minimal Lambda deployment. + +The `/{binary}/tests` folders are integration tests involving the generated clients. + +## Build + +Since this example requires both the server and client SDK to be code-generated +from their [model](/codegen-server-test/model/pokemon.smithy), a Makefile is +provided to build and run the service. Just run `make` to prepare the first +build. + +Once the example has been built successfully the first time, idiomatic `cargo` +can be used directly. + +`make distclean` can be used for a complete cleanup of all artefacts. + +## Run + +To run a binary use + +```bash +cargo run -p $BINARY +``` + +CLI arguments can be passed to the servers, use + +```bash +cargo run -p $BINARY -- --help +``` + +for more information. + +## Test + +`cargo test` can be used to spawn a service and run some simple integration +tests against it. Use `-p $BINARY` to filter by package. + +More info can be found in the `tests` folder of each package. + +## Benchmarks + +Please see [BENCHMARKS.md](/examples/BENCHMARKS.md). diff --git a/examples/pokemon-service-common/Cargo.toml b/examples/pokemon-service-common/Cargo.toml new file mode 100644 index 0000000000..704055c813 --- /dev/null +++ b/examples/pokemon-service-common/Cargo.toml @@ -0,0 +1,24 @@ +[package] +name = "pokemon-service-common" +version = "0.1.0" +edition = "2021" +publish = false +authors = ["Smithy-rs Server Team "] +description = "A smithy Rust service to retrieve information about Pokémon." + +[dependencies] +async-stream = "0.3" +http = "0.2.9" +rand = "0.8" +tracing = "0.1" +tracing-subscriber = { version = "0.3.16", features = ["env-filter", "json"] } +tokio = { version = "1", default-features = false, features = ["time"] } + +# Local paths +aws-smithy-http = { path = "../../rust-runtime/aws-smithy-http" } +aws-smithy-http-server = { path = "../../rust-runtime/aws-smithy-http-server" } +pokemon-service-client = { path = "../pokemon-service-client" } +pokemon-service-server-sdk = { path = "../pokemon-service-server-sdk" } + +[dev-dependencies] +tower = "0.4" diff --git a/rust-runtime/aws-smithy-http-server/examples/pokemon-service/src/lib.rs b/examples/pokemon-service-common/src/lib.rs similarity index 91% rename from rust-runtime/aws-smithy-http-server/examples/pokemon-service/src/lib.rs rename to examples/pokemon-service-common/src/lib.rs index 7a4d868da0..f431270da6 100644 --- a/rust-runtime/aws-smithy-http-server/examples/pokemon-service/src/lib.rs +++ b/examples/pokemon-service-common/src/lib.rs @@ -10,17 +10,19 @@ use std::{ collections::HashMap, convert::TryInto, + process::Child, sync::{atomic::AtomicUsize, Arc}, }; use async_stream::stream; +use aws_smithy_http::operation::Request; use aws_smithy_http_server::Extension; -use pokemon_service_server_sdk::{error, input, model, model::CapturingPayload, output, types::Blob}; +use pokemon_service_server_sdk::{ + error, input, model, model::CapturingPayload, output, types::Blob, +}; use rand::Rng; use tracing_subscriber::{prelude::*, EnvFilter}; -pub mod plugin; - const PIKACHU_ENGLISH_FLAVOR_TEXT: &str = "When several of these Pokémon gather, their electricity could build and cause lightning storms."; const PIKACHU_SPANISH_FLAVOR_TEXT: &str = @@ -30,13 +32,37 @@ const PIKACHU_ITALIAN_FLAVOR_TEXT: &str = const PIKACHU_JAPANESE_FLAVOR_TEXT: &str = "ほっぺたの りょうがわに ちいさい でんきぶくろを もつ。ピンチのときに ほうでんする。"; +/// Rewrites the base URL of a request +pub fn rewrite_base_url(base_url: String) -> impl Fn(Request) -> Request + Clone { + move |mut req| { + let http_req = req.http_mut(); + let uri = format!("{base_url}{}", http_req.uri().path()); + *http_req.uri_mut() = uri.parse().unwrap(); + req + } +} + +/// Kills [`Child`] process when dropped. +#[derive(Debug)] +#[must_use] +pub struct ChildDrop(pub Child); + +impl Drop for ChildDrop { + fn drop(&mut self) { + self.0.kill().expect("failed to kill process") + } +} + /// Setup `tracing::subscriber` to read the log level from RUST_LOG environment variable. pub fn setup_tracing() { let format = tracing_subscriber::fmt::layer().json(); let filter = EnvFilter::try_from_default_env() .or_else(|_| EnvFilter::try_new("info")) .unwrap(); - tracing_subscriber::registry().with(format).with(filter).init(); + tracing_subscriber::registry() + .with(format) + .with(filter) + .init(); } /// Structure holding the translations for a Pokémon description. @@ -134,7 +160,10 @@ pub async fn get_pokemon_species( input: input::GetPokemonSpeciesInput, state: Extension>, ) -> Result { - state.0.call_count.fetch_add(1, std::sync::atomic::Ordering::SeqCst); + state + .0 + .call_count + .fetch_add(1, std::sync::atomic::Ordering::SeqCst); // We only support retrieving information about Pikachu. let pokemon = state.0.pokemons_translations.get(&input.name); match pokemon.as_ref() { @@ -215,7 +244,9 @@ pub async fn capture_pokemon( ) -> Result { if input.region != "Kanto" { return Err(error::CapturePokemonError::UnsupportedRegionError( - error::UnsupportedRegionError { region: input.region }, + error::UnsupportedRegionError { + region: input.region, + }, )); } let output_stream = stream! { @@ -307,7 +338,10 @@ mod tests { .find(|flavor_text| flavor_text.language == model::Language::Spanish) .unwrap(); - assert_eq!(PIKACHU_SPANISH_FLAVOR_TEXT, actual_spanish_flavor_text.flavor_text()); + assert_eq!( + PIKACHU_SPANISH_FLAVOR_TEXT, + actual_spanish_flavor_text.flavor_text() + ); let input = input::GetServerStatisticsInput {}; let stats = get_server_statistics(input, Extension(state.clone())).await; diff --git a/rust-runtime/aws-smithy-http-server/examples/pokemon-service/tests/plugins_execution_order.rs b/examples/pokemon-service-common/tests/plugins_execution_order.rs similarity index 88% rename from rust-runtime/aws-smithy-http-server/examples/pokemon-service/tests/plugins_execution_order.rs rename to examples/pokemon-service-common/tests/plugins_execution_order.rs index 7585855564..78dccecde1 100644 --- a/rust-runtime/aws-smithy-http-server/examples/pokemon-service/tests/plugins_execution_order.rs +++ b/examples/pokemon-service-common/tests/plugins_execution_order.rs @@ -2,20 +2,25 @@ * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. * SPDX-License-Identifier: Apache-2.0 */ + +use std::{ + ops::Deref, + sync::Arc, + sync::Mutex, + task::{Context, Poll}, +}; + use aws_smithy_http::body::SdkBody; -use aws_smithy_http_server::operation::Operation; -use aws_smithy_http_server::plugin::{Plugin, PluginPipeline}; -use hyper::http; -use pokemon_service::do_nothing; -use pokemon_service_client::operation::do_nothing::DoNothingInput; -use pokemon_service_client::Config; -use std::ops::Deref; -use std::sync::Arc; -use std::sync::Mutex; -use std::task::{Context, Poll}; +use aws_smithy_http_server::{ + operation::Operation, + plugin::{Plugin, PluginPipeline}, +}; use tower::layer::util::Stack; use tower::{Layer, Service}; +use pokemon_service_client::{operation::do_nothing::DoNothingInput, Config}; +use pokemon_service_common::do_nothing; + trait OperationExt { /// Convert an SDK operation into an `http::Request`. fn into_http(self) -> http::Request; @@ -59,7 +64,10 @@ struct SentinelPlugin { impl SentinelPlugin { pub fn new(name: &'static str, output: Arc>>) -> Self { - Self { name, output: output } + Self { + name, + output: output, + } } } diff --git a/examples/pokemon-service-lambda/Cargo.toml b/examples/pokemon-service-lambda/Cargo.toml new file mode 100644 index 0000000000..96a2d18230 --- /dev/null +++ b/examples/pokemon-service-lambda/Cargo.toml @@ -0,0 +1,21 @@ +[package] +name = "pokemon-service-lambda" +version = "0.1.0" +edition = "2021" +publish = false +authors = ["Smithy-rs Server Team "] +description = "A smithy Rust service to retrieve information about Pokémon via Lambda." + +[dependencies] +async-stream = "0.3.4" +clap = { version = "4.1.11", features = ["derive"] } +hyper = {version = "0.14.25", features = ["server"] } +tokio = "1.26.0" +tracing = "0.1" + +lambda_http = "0.7.3" + +# Local paths +aws-smithy-http-server = { path = "../../rust-runtime/aws-smithy-http-server", features = ["aws-lambda"] } +pokemon-service-server-sdk = { path = "../pokemon-service-server-sdk/" } +pokemon-service-common = { path = "../pokemon-service-common/" } diff --git a/examples/pokemon-service-lambda/src/lib.rs b/examples/pokemon-service-lambda/src/lib.rs new file mode 100644 index 0000000000..b2034d6566 --- /dev/null +++ b/examples/pokemon-service-lambda/src/lib.rs @@ -0,0 +1,33 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +use std::sync::Arc; + +use aws_smithy_http_server::{request::lambda::Context, Extension}; + +use pokemon_service_common::State; +use pokemon_service_server_sdk::{ + error::{GetStorageError, StorageAccessNotAuthorized}, + input::GetStorageInput, + output::GetStorageOutput, +}; + +/// Retrieves the user's storage and logs the lambda request ID. +pub async fn get_storage_lambda( + input: GetStorageInput, + _state: Extension>, + context: Context, +) -> Result { + tracing::debug!(request_id = %context.request_id, "attempting to authenticate storage user"); + + // We currently only support Ash and he has nothing stored + if !(input.user == "ash" && input.passcode == "pikachu123") { + tracing::debug!("authentication failed"); + return Err(GetStorageError::StorageAccessNotAuthorized( + StorageAccessNotAuthorized {}, + )); + } + Ok(GetStorageOutput { collection: vec![] }) +} diff --git a/examples/pokemon-service-lambda/src/main.rs b/examples/pokemon-service-lambda/src/main.rs new file mode 100644 index 0000000000..2e80cb9802 --- /dev/null +++ b/examples/pokemon-service-lambda/src/main.rs @@ -0,0 +1,39 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +use std::sync::Arc; + +use aws_smithy_http_server::{routing::LambdaHandler, AddExtensionLayer}; + +use pokemon_service_common::{ + capture_pokemon, check_health, do_nothing, get_pokemon_species, get_server_statistics, State, +}; +use pokemon_service_lambda::get_storage_lambda; +use pokemon_service_server_sdk::PokemonService; + +#[tokio::main] +pub async fn main() { + let app = PokemonService::builder_without_plugins() + // Build a registry containing implementations to all the operations in the service. These + // are async functions or async closures that take as input the operation's input and + // return the operation's output. + .get_pokemon_species(get_pokemon_species) + .get_storage(get_storage_lambda) + .get_server_statistics(get_server_statistics) + .capture_pokemon(capture_pokemon) + .do_nothing(do_nothing) + .check_health(check_health) + .build() + .expect("failed to build an instance of PokemonService") + // Set up shared state and middlewares. + .layer(&AddExtensionLayer::new(Arc::new(State::default()))); + + let handler = LambdaHandler::new(app); + let lambda = lambda_http::run(handler); + + if let Err(err) = lambda.await { + eprintln!("lambda error: {}", err); + } +} diff --git a/rust-runtime/aws-smithy-http-server/examples/pokemon-service/tests/fixtures/example-apigw-request.json b/examples/pokemon-service-lambda/tests/fixtures/example-apigw-request.json similarity index 100% rename from rust-runtime/aws-smithy-http-server/examples/pokemon-service/tests/fixtures/example-apigw-request.json rename to examples/pokemon-service-lambda/tests/fixtures/example-apigw-request.json diff --git a/examples/pokemon-service-tls/Cargo.toml b/examples/pokemon-service-tls/Cargo.toml new file mode 100644 index 0000000000..ac3b32b7fe --- /dev/null +++ b/examples/pokemon-service-tls/Cargo.toml @@ -0,0 +1,36 @@ +[package] +name = "pokemon-service-tls" +version = "0.1.0" +edition = "2021" +publish = false +authors = ["Smithy-rs Server Team "] +description = "A smithy Rust service to retrieve information about Pokémon." + +[dependencies] +clap = { version = "4.1.11", features = ["derive"] } +hyper = { version = "0.14.25", features = ["server"] } +tokio = "1.26.0" + +# These dependencies are only required for the `pokemon-service-tls` program. +tls-listener = { version = "0.6.0", features = ["rustls", "hyper-h2"] } +tokio-rustls = "0.23.4" +rustls-pemfile = "1.0.2" +futures-util = { version = "0.3.27", default-features = false } + +# Local paths +aws-smithy-http-server = { path = "../../rust-runtime/aws-smithy-http-server" } +pokemon-service-server-sdk = { path = "../pokemon-service-server-sdk/" } +pokemon-service-common = { path = "../pokemon-service-common/" } + +[dev-dependencies] +assert_cmd = "2.0" +serial_test = "1.0.0" + +# This dependency is only required for testing the `pokemon-service-tls` program. +hyper-rustls = { version = "0.23.2", features = ["http2"] } + +# Local paths +aws-smithy-client = { path = "../../rust-runtime/aws-smithy-client/", features = ["rustls"] } +aws-smithy-http = { path = "../../rust-runtime/aws-smithy-http/" } +aws-smithy-types = { path = "../../rust-runtime/aws-smithy-types/" } +pokemon-service-client = { path = "../pokemon-service-client/" } diff --git a/examples/pokemon-service-tls/src/lib.rs b/examples/pokemon-service-tls/src/lib.rs new file mode 100644 index 0000000000..d5006504e1 --- /dev/null +++ b/examples/pokemon-service-tls/src/lib.rs @@ -0,0 +1,13 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +// Defaults shared between `main.rs` and `/tests`. +pub const DEFAULT_TEST_KEY: &str = + concat!(env!("CARGO_MANIFEST_DIR"), "/tests/testdata/localhost.key"); +pub const DEFAULT_TEST_CERT: &str = + concat!(env!("CARGO_MANIFEST_DIR"), "/tests/testdata/localhost.crt"); +pub const DEFAULT_ADDRESS: &str = "127.0.0.1"; +pub const DEFAULT_PORT: u16 = 13734; +pub const DEFAULT_DOMAIN: &str = "localhost"; diff --git a/rust-runtime/aws-smithy-http-server/examples/pokemon-service/src/bin/pokemon-service-tls.rs b/examples/pokemon-service-tls/src/main.rs similarity index 87% rename from rust-runtime/aws-smithy-http-server/examples/pokemon-service/src/bin/pokemon-service-tls.rs rename to examples/pokemon-service-tls/src/main.rs index 34ccd0c6f1..2a9ef679d2 100644 --- a/rust-runtime/aws-smithy-http-server/examples/pokemon-service/src/bin/pokemon-service-tls.rs +++ b/examples/pokemon-service-tls/src/main.rs @@ -22,39 +22,37 @@ // note that by default created certificates will be unknown and you should use `-k|--insecure` // flag while making requests with cURL or you can run `mkcert -install` to trust certificates created by `mkcert`. -use std::fs::File; -use std::future; -use std::io::BufReader; -use std::net::SocketAddr; -use std::sync::Arc; +use std::{fs::File, future, io::BufReader, net::SocketAddr, sync::Arc}; -use aws_smithy_http_server::{plugin::PluginPipeline, AddExtensionLayer}; +use aws_smithy_http_server::AddExtensionLayer; use clap::Parser; use futures_util::stream::StreamExt; -use pokemon_service::{ - capture_pokemon, check_health, do_nothing, get_pokemon_species, get_server_statistics, get_storage, - plugin::PrintExt, setup_tracing, State, -}; -use pokemon_service_server_sdk::PokemonService; use tokio_rustls::{ rustls::{Certificate, PrivateKey, ServerConfig}, TlsAcceptor, }; +use pokemon_service_common::{ + capture_pokemon, check_health, do_nothing, get_pokemon_species, get_server_statistics, + get_storage, setup_tracing, State, +}; +use pokemon_service_server_sdk::PokemonService; +use pokemon_service_tls::{DEFAULT_ADDRESS, DEFAULT_PORT, DEFAULT_TEST_CERT, DEFAULT_TEST_KEY}; + #[derive(Parser, Debug)] #[clap(author, version, about, long_about = None)] struct Args { /// Hyper server bind address. - #[clap(short, long, action, default_value = "127.0.0.1")] + #[clap(short, long, action, default_value = DEFAULT_ADDRESS)] address: String, /// Hyper server bind port. - #[clap(short, long, action, default_value = "13734")] + #[clap(short, long, action, default_value_t = DEFAULT_PORT)] port: u16, /// Hyper server TLS certificate path. Must be a PEM file. - #[clap(long, default_value = "")] + #[clap(long, default_value = DEFAULT_TEST_CERT)] tls_cert_path: String, /// Hyper server TLS private key path. Must be a PEM file. - #[clap(long, default_value = "")] + #[clap(long, default_value = DEFAULT_TEST_KEY)] tls_key_path: String, } @@ -62,9 +60,8 @@ struct Args { pub async fn main() { let args = Args::parse(); setup_tracing(); - // Apply the `PrintPlugin` defined in `plugin.rs` - let plugins = PluginPipeline::new().print(); - let app = PokemonService::builder_with_plugins(plugins) + + let app = PokemonService::builder_without_plugins() // Build a registry containing implementations to all the operations in the service. These // are async functions or async closures that take as input the operation's input and // return the operation's output. @@ -96,7 +93,8 @@ pub async fn main() { future::ready(true) } }); - let server = hyper::Server::builder(hyper::server::accept::from_stream(listener)).serve(app.into_make_service()); + let server = hyper::Server::builder(hyper::server::accept::from_stream(listener)) + .serve(app.into_make_service()); if let Err(err) = server.await { eprintln!("server error: {}", err); } diff --git a/examples/pokemon-service-tls/tests/common/mod.rs b/examples/pokemon-service-tls/tests/common/mod.rs new file mode 100644 index 0000000000..746b7823e2 --- /dev/null +++ b/examples/pokemon-service-tls/tests/common/mod.rs @@ -0,0 +1,58 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +use std::{fs::File, io::BufReader, process::Command, time::Duration}; + +use assert_cmd::prelude::*; +use aws_smithy_client::{ + erase::{DynConnector, DynMiddleware}, + hyper_ext::Adapter, +}; +use tokio::time::sleep; + +use pokemon_service_client::{Builder, Client, Config}; +use pokemon_service_common::{rewrite_base_url, ChildDrop}; +use pokemon_service_tls::{DEFAULT_DOMAIN, DEFAULT_PORT, DEFAULT_TEST_CERT}; + +pub async fn run_server() -> ChildDrop { + let child = Command::cargo_bin("pokemon-service-tls") + .unwrap() + .spawn() + .unwrap(); + + sleep(Duration::from_millis(500)).await; + + ChildDrop(child) +} + +// Returns a client that only talks through https and http2 connections. +// It is useful in testing whether our server can talk to http2. +pub fn client_http2_only() -> Client> { + // Create custom cert store and add our test certificate to prevent unknown cert issues. + let mut reader = + BufReader::new(File::open(DEFAULT_TEST_CERT).expect("could not open certificate")); + let certs = rustls_pemfile::certs(&mut reader).expect("could not parse certificate"); + let mut roots = tokio_rustls::rustls::RootCertStore::empty(); + roots.add_parsable_certificates(&certs); + + let connector = hyper_rustls::HttpsConnectorBuilder::new() + .with_tls_config( + tokio_rustls::rustls::ClientConfig::builder() + .with_safe_defaults() + .with_root_certificates(roots) + .with_no_client_auth(), + ) + .https_only() + .enable_http2() + .build(); + + let base_url = format!("https://{DEFAULT_DOMAIN}:{DEFAULT_PORT}"); + let raw_client = Builder::new() + .connector(DynConnector::new(Adapter::builder().build(connector))) + .middleware_fn(rewrite_base_url(base_url)) + .build_dyn(); + let config = Config::builder().build(); + Client::with_config(raw_client, config) +} diff --git a/examples/pokemon-service-tls/tests/http2.rs b/examples/pokemon-service-tls/tests/http2.rs new file mode 100644 index 0000000000..32c0fba08c --- /dev/null +++ b/examples/pokemon-service-tls/tests/http2.rs @@ -0,0 +1,14 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +pub mod common; + +#[tokio::test] +async fn test_check_health_http2() { + let _child = common::run_server().await; + let client = common::client_http2_only(); + + let _check_health = client.check_health().send().await.unwrap(); +} diff --git a/rust-runtime/aws-smithy-http-server/examples/pokemon-service/tests/testdata/localhost.crt b/examples/pokemon-service-tls/tests/testdata/localhost.crt similarity index 100% rename from rust-runtime/aws-smithy-http-server/examples/pokemon-service/tests/testdata/localhost.crt rename to examples/pokemon-service-tls/tests/testdata/localhost.crt diff --git a/rust-runtime/aws-smithy-http-server/examples/pokemon-service/tests/testdata/localhost.key b/examples/pokemon-service-tls/tests/testdata/localhost.key similarity index 100% rename from rust-runtime/aws-smithy-http-server/examples/pokemon-service/tests/testdata/localhost.key rename to examples/pokemon-service-tls/tests/testdata/localhost.key diff --git a/examples/pokemon-service/Cargo.toml b/examples/pokemon-service/Cargo.toml new file mode 100644 index 0000000000..30839509e5 --- /dev/null +++ b/examples/pokemon-service/Cargo.toml @@ -0,0 +1,34 @@ +[package] +name = "pokemon-service" +version = "0.1.0" +edition = "2021" +publish = false +authors = ["Smithy-rs Server Team "] +description = "A smithy Rust service to retrieve information about Pokémon." + +[dependencies] +clap = { version = "4.1.11", features = ["derive"] } +hyper = {version = "0.14.25", features = ["server"] } +tokio = "1.26.0" +tower = "0.4" +tracing = "0.1" + +# Local paths +aws-smithy-http-server = { path = "../../rust-runtime/aws-smithy-http-server", features = ["request-id"] } +pokemon-service-server-sdk = { path = "../pokemon-service-server-sdk/" } +pokemon-service-common = { path = "../pokemon-service-common/" } + +[dev-dependencies] +assert_cmd = "2.0" +async-stream = "0.3" +rand = "0.8.5" +serial_test = "1.0.0" + +# This dependency is only required for testing the `pokemon-service-tls` program. +hyper-rustls = { version = "0.23.2", features = ["http2"] } + +# Local paths +aws-smithy-client = { path = "../../rust-runtime/aws-smithy-client/", features = ["rustls"] } +aws-smithy-http = { path = "../../rust-runtime/aws-smithy-http/" } +aws-smithy-types = { path = "../../rust-runtime/aws-smithy-types/" } +pokemon-service-client = { path = "../pokemon-service-client/" } diff --git a/examples/pokemon-service/src/lib.rs b/examples/pokemon-service/src/lib.rs new file mode 100644 index 0000000000..b6521213a1 --- /dev/null +++ b/examples/pokemon-service/src/lib.rs @@ -0,0 +1,59 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +use std::net::{IpAddr, SocketAddr}; + +use aws_smithy_http_server::request::{connect_info::ConnectInfo, request_id::ServerRequestId}; +use pokemon_service_server_sdk::{ + error::{GetStorageError, StorageAccessNotAuthorized}, + input::{DoNothingInput, GetStorageInput}, + output::{DoNothingOutput, GetStorageOutput}, +}; + +// Defaults shared between `main.rs` and `/tests`. +pub const DEFAULT_ADDRESS: &str = "127.0.0.1"; +pub const DEFAULT_PORT: u16 = 13734; + +/// Logs the request IDs to `DoNothing` operation. +pub async fn do_nothing_but_log_request_ids( + _input: DoNothingInput, + request_id: ServerRequestId, +) -> DoNothingOutput { + tracing::debug!(%request_id, "do nothing"); + DoNothingOutput {} +} + +/// Retrieves the user's storage. No authentication required for locals. +pub async fn get_storage_with_local_approved( + input: GetStorageInput, + connect_info: ConnectInfo, +) -> Result { + tracing::debug!("attempting to authenticate storage user"); + + if !(input.user == "ash" && input.passcode == "pikachu123") { + tracing::debug!("authentication failed"); + return Err(GetStorageError::StorageAccessNotAuthorized( + StorageAccessNotAuthorized {}, + )); + } + + // We support trainers in our local gym + let local = connect_info.0.ip() == "127.0.0.1".parse::().unwrap(); + if local { + tracing::info!("welcome back"); + return Ok(GetStorageOutput { + collection: vec![ + String::from("bulbasaur"), + String::from("charmander"), + String::from("squirtle"), + String::from("pikachu"), + ], + }); + } + + Ok(GetStorageOutput { + collection: vec![String::from("pikachu")], + }) +} diff --git a/rust-runtime/aws-smithy-http-server/examples/pokemon-service/src/bin/pokemon-service.rs b/examples/pokemon-service/src/main.rs similarity index 59% rename from rust-runtime/aws-smithy-http-server/examples/pokemon-service/src/bin/pokemon-service.rs rename to examples/pokemon-service/src/main.rs index 402e57555a..f26e6d14ba 100644 --- a/rust-runtime/aws-smithy-http-server/examples/pokemon-service/src/bin/pokemon-service.rs +++ b/examples/pokemon-service/src/main.rs @@ -3,14 +3,22 @@ * SPDX-License-Identifier: Apache-2.0 */ -// This program is exported as a binary named `pokemon-service`. +mod plugin; + use std::{net::SocketAddr, sync::Arc}; -use aws_smithy_http_server::{extension::OperationExtensionExt, plugin::PluginPipeline, AddExtensionLayer}; +use aws_smithy_http_server::{ + extension::OperationExtensionExt, instrumentation::InstrumentExt, plugin::PluginPipeline, + request::request_id::ServerRequestIdProviderLayer, AddExtensionLayer, +}; use clap::Parser; + +use plugin::PrintExt; use pokemon_service::{ - capture_pokemon, check_health, do_nothing, get_pokemon_species, get_server_statistics, get_storage, - plugin::PrintExt, setup_tracing, State, + do_nothing_but_log_request_ids, get_storage_with_local_approved, DEFAULT_ADDRESS, DEFAULT_PORT, +}; +use pokemon_service_common::{ + capture_pokemon, check_health, get_pokemon_species, get_server_statistics, setup_tracing, State, }; use pokemon_service_server_sdk::PokemonService; @@ -18,10 +26,10 @@ use pokemon_service_server_sdk::PokemonService; #[clap(author, version, about, long_about = None)] struct Args { /// Hyper server bind address. - #[clap(short, long, action, default_value = "127.0.0.1")] + #[clap(short, long, action, default_value = DEFAULT_ADDRESS)] address: String, /// Hyper server bind port. - #[clap(short, long, action, default_value = "13734")] + #[clap(short, long, action, default_value_t = DEFAULT_PORT)] port: u16, } @@ -29,33 +37,44 @@ struct Args { pub async fn main() { let args = Args::parse(); setup_tracing(); + let plugins = PluginPipeline::new() // Apply the `PrintPlugin` defined in `plugin.rs` .print() // Apply the `OperationExtensionPlugin` defined in `aws_smithy_http_server::extension`. This allows other // plugins or tests to access a `aws_smithy_http_server::extension::OperationExtension` from // `Response::extensions`, or infer routing failure when it's missing. - .insert_operation_extension(); + .insert_operation_extension() + // Adds `tracing` spans and events to the request lifecycle. + .instrument(); let app = PokemonService::builder_with_plugins(plugins) // Build a registry containing implementations to all the operations in the service. These // are async functions or async closures that take as input the operation's input and // return the operation's output. .get_pokemon_species(get_pokemon_species) - .get_storage(get_storage) + .get_storage(get_storage_with_local_approved) .get_server_statistics(get_server_statistics) .capture_pokemon(capture_pokemon) - .do_nothing(do_nothing) + .do_nothing(do_nothing_but_log_request_ids) .check_health(check_health) .build() - .expect("failed to build an instance of PokemonService") - // Setup shared state and middlewares. - .layer(&AddExtensionLayer::new(Arc::new(State::default()))); + .expect("failed to build an instance of PokemonService"); - // Start the [`hyper::Server`]. + let app = app + // Setup shared state and middlewares. + .layer(&AddExtensionLayer::new(Arc::new(State::default()))) + // Add request IDs + .layer(&ServerRequestIdProviderLayer::new()); + + // Using `into_make_service_with_connect_info`, rather than `into_make_service`, to adjoin the `SocketAddr` + // connection info. + let make_app = app.into_make_service_with_connect_info::(); + + // Bind the application to a socket. let bind: SocketAddr = format!("{}:{}", args.address, args.port) .parse() .expect("unable to parse the server bind address and port"); - let server = hyper::Server::bind(&bind).serve(app.into_make_service()); + let server = hyper::Server::bind(&bind).serve(make_app); // Run forever-ish... if let Err(err) = server.await { diff --git a/rust-runtime/aws-smithy-http-server/examples/pokemon-service/src/plugin.rs b/examples/pokemon-service/src/plugin.rs similarity index 100% rename from rust-runtime/aws-smithy-http-server/examples/pokemon-service/src/plugin.rs rename to examples/pokemon-service/src/plugin.rs diff --git a/examples/pokemon-service/tests/common/mod.rs b/examples/pokemon-service/tests/common/mod.rs new file mode 100644 index 0000000000..b21eb2a78b --- /dev/null +++ b/examples/pokemon-service/tests/common/mod.rs @@ -0,0 +1,35 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +use std::{process::Command, time::Duration}; + +use assert_cmd::prelude::*; +use aws_smithy_client::erase::{DynConnector, DynMiddleware}; +use tokio::time::sleep; + +use pokemon_service::{DEFAULT_ADDRESS, DEFAULT_PORT}; +use pokemon_service_client::{Builder, Client, Config}; +use pokemon_service_common::{rewrite_base_url, ChildDrop}; + +pub async fn run_server() -> ChildDrop { + let child = Command::cargo_bin("pokemon-service") + .unwrap() + .spawn() + .unwrap(); + + sleep(Duration::from_millis(500)).await; + + ChildDrop(child) +} + +pub fn client() -> Client> { + let base_url = format!("http://{DEFAULT_ADDRESS}:{DEFAULT_PORT}"); + let raw_client = Builder::new() + .rustls_connector(Default::default()) + .middleware_fn(rewrite_base_url(base_url)) + .build_dyn(); + let config = Config::builder().build(); + Client::with_config(raw_client, config) +} diff --git a/rust-runtime/aws-smithy-http-server/examples/pokemon-service/tests/simple_integration_test.rs b/examples/pokemon-service/tests/event_streaming.rs similarity index 57% rename from rust-runtime/aws-smithy-http-server/examples/pokemon-service/tests/simple_integration_test.rs rename to examples/pokemon-service/tests/event_streaming.rs index a853d6dd1e..664827620b 100644 --- a/rust-runtime/aws-smithy-http-server/examples/pokemon-service/tests/simple_integration_test.rs +++ b/examples/pokemon-service/tests/event_streaming.rs @@ -3,24 +3,21 @@ * SPDX-License-Identifier: Apache-2.0 */ -// Files here are for running integration tests. -// These tests only have access to your crate's public API. -// See: https://doc.rust-lang.org/book/ch11-03-test-organization.html#integration-tests - -use crate::helpers::{client, client_http2_only, PokemonService}; +pub mod common; use async_stream::stream; -use aws_smithy_types::error::display::DisplayErrorContext; -use pokemon_service_client::{ - error::SdkError, - operation::get_storage::GetStorageError, - types::error::{AttemptCapturingPokemonEventError, MasterBallUnsuccessful, StorageAccessNotAuthorized}, - types::{AttemptCapturingPokemonEvent, CapturingEvent, CapturingPayload}, -}; use rand::Rng; use serial_test::serial; -mod helpers; +use pokemon_service_client::types::{ + error::{AttemptCapturingPokemonEventError, MasterBallUnsuccessful}, + AttemptCapturingPokemonEvent, CapturingEvent, CapturingPayload, +}; + +fn get_pokemon_to_capture() -> String { + let pokemons = vec!["Charizard", "Pikachu", "Regieleki"]; + pokemons[rand::thread_rng().gen_range(0..pokemons.len())].to_string() +} fn get_pokeball() -> String { let random = rand::thread_rng().gen_range(0..100); @@ -36,82 +33,11 @@ fn get_pokeball() -> String { pokeball.to_string() } -fn get_pokemon_to_capture() -> String { - let pokemons = vec!["Charizard", "Pikachu", "Regieleki"]; - pokemons[rand::thread_rng().gen_range(0..pokemons.len())].to_string() -} - -#[tokio::test] -#[serial] -async fn test_check_health() { - let _program = PokemonService::run().await; - - let _check_health = client().check_health().send().await.unwrap(); -} - -#[tokio::test] -#[serial] -async fn test_check_health_http2() { - // Make sure our server can serve http2 - let _program = PokemonService::run_https().await; - let _check_health = client_http2_only().check_health().send().await.unwrap(); -} - -#[tokio::test] -#[serial] -async fn simple_integration_test() { - let _program = PokemonService::run().await; - - let service_statistics_out = client().get_server_statistics().send().await.unwrap(); - assert_eq!(0, service_statistics_out.calls_count.unwrap()); - - let pokemon_species_output = client().get_pokemon_species().name("pikachu").send().await.unwrap(); - assert_eq!("pikachu", pokemon_species_output.name().unwrap()); - - let service_statistics_out = client().get_server_statistics().send().await.unwrap(); - assert_eq!(1, service_statistics_out.calls_count.unwrap()); - - let storage_err = client().get_storage().user("ash").passcode("pikachu321").send().await; - let has_not_authorized_error = if let Err(SdkError::ServiceError(context)) = storage_err { - matches!( - context.err(), - GetStorageError::StorageAccessNotAuthorized(StorageAccessNotAuthorized { .. }), - ) - } else { - false - }; - assert!(has_not_authorized_error, "expected NotAuthorized error"); - - let storage_out = client() - .get_storage() - .user("ash") - .passcode("pikachu123") - .send() - .await - .unwrap(); - assert_eq!(Some(vec![]), storage_out.collection); - - let pokemon_species_error = client() - .get_pokemon_species() - .name("some_pokémon") - .send() - .await - .unwrap_err(); - let message = DisplayErrorContext(pokemon_species_error).to_string(); - let expected = r#"ResourceNotFoundError [ResourceNotFoundException]: Requested Pokémon not available"#; - assert!( - message.contains(expected), - "expected '{message}' to contain '{expected}'" - ); - - let service_statistics_out = client().get_server_statistics().send().await.unwrap(); - assert_eq!(2, service_statistics_out.calls_count.unwrap()); -} - #[tokio::test] #[serial] async fn event_stream_test() { - let _program = PokemonService::run().await; + let _child = common::run_server().await; + let client = common::client(); let mut team = vec![]; let input_stream = stream! { @@ -145,7 +71,7 @@ async fn event_stream_test() { }; // Throw many! - let mut output = client() + let mut output = common::client() .capture_pokemon() .region("Kanto") .events(input_stream.into()) @@ -156,7 +82,13 @@ async fn event_stream_test() { match output.events.recv().await { Ok(Some(capture)) => { let pokemon = capture.as_event().unwrap().name.as_ref().unwrap().clone(); - let pokedex = capture.as_event().unwrap().pokedex_update.as_ref().unwrap().clone(); + let pokedex = capture + .as_event() + .unwrap() + .pokedex_update + .as_ref() + .unwrap() + .clone(); let shiny = if *capture.as_event().unwrap().shiny.as_ref().unwrap() { "" } else { @@ -190,7 +122,7 @@ async fn event_stream_test() { .build() )) }; - let mut output = client() + let mut output = client .capture_pokemon() .region("Kanto") .events(input_stream.into()) @@ -200,7 +132,13 @@ async fn event_stream_test() { match output.events.recv().await { Ok(Some(capture)) => { let pokemon = capture.as_event().unwrap().name.as_ref().unwrap().clone(); - let pokedex = capture.as_event().unwrap().pokedex_update.as_ref().unwrap().clone(); + let pokedex = capture + .as_event() + .unwrap() + .pokedex_update + .as_ref() + .unwrap() + .clone(); let shiny = if *capture.as_event().unwrap().shiny.as_ref().unwrap() { "" } else { diff --git a/examples/pokemon-service/tests/simple.rs b/examples/pokemon-service/tests/simple.rs new file mode 100644 index 0000000000..1c86d3cf10 --- /dev/null +++ b/examples/pokemon-service/tests/simple.rs @@ -0,0 +1,84 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +use pokemon_service_client::{ + error::{DisplayErrorContext, SdkError}, + operation::get_storage::GetStorageError, + types::error::StorageAccessNotAuthorized, +}; +use serial_test::serial; + +pub mod common; + +#[tokio::test] +#[serial] +async fn simple_integration_test() { + let _child = common::run_server().await; + let client = common::client(); + + let service_statistics_out = client.get_server_statistics().send().await.unwrap(); + assert_eq!(0, service_statistics_out.calls_count.unwrap()); + + let pokemon_species_output = client + .get_pokemon_species() + .name("pikachu") + .send() + .await + .unwrap(); + assert_eq!("pikachu", pokemon_species_output.name().unwrap()); + + let service_statistics_out = client.get_server_statistics().send().await.unwrap(); + assert_eq!(1, service_statistics_out.calls_count.unwrap()); + + let storage_err = client + .get_storage() + .user("ash") + .passcode("pikachu321") + .send() + .await; + let has_not_authorized_error = if let Err(SdkError::ServiceError(context)) = storage_err { + matches!( + context.err(), + GetStorageError::StorageAccessNotAuthorized(StorageAccessNotAuthorized { .. }), + ) + } else { + false + }; + assert!(has_not_authorized_error, "expected NotAuthorized error"); + + let storage_out = client + .get_storage() + .user("ash") + .passcode("pikachu123") + .send() + .await + .unwrap(); + assert_eq!( + Some(vec![ + "bulbasaur".to_string(), + "charmander".to_string(), + "squirtle".to_string(), + "pikachu".to_string() + ]), + storage_out.collection + ); + + let pokemon_species_error = client + .get_pokemon_species() + .name("some_pokémon") + .send() + .await + .unwrap_err(); + let message = DisplayErrorContext(pokemon_species_error).to_string(); + let expected = + r#"ResourceNotFoundError [ResourceNotFoundException]: Requested Pokémon not available"#; + assert!( + message.contains(expected), + "expected '{message}' to contain '{expected}'" + ); + + let service_statistics_out = client.get_server_statistics().send().await.unwrap(); + assert_eq!(2, service_statistics_out.calls_count.unwrap()); +} diff --git a/rust-runtime/aws-smithy-http-server/examples/README.md b/rust-runtime/aws-smithy-http-server/examples/README.md deleted file mode 100644 index f3dd3e4b5f..0000000000 --- a/rust-runtime/aws-smithy-http-server/examples/README.md +++ /dev/null @@ -1,32 +0,0 @@ -# Smithy Rust Server SDK example - -This folder contains an example service called Pokémon Service used to showcase -the service framework capabilities and to run benchmarks. - -## Build - -Since this example requires both the server and client SDK to be code-generated -from their [model](/codegen-server-test/model/pokemon.smithy), a Makefile is -provided to build and run the service. Just run `make` to prepare the first -build. - -Once the example has been built successfully the first time, idiomatic `cargo` -can be used directly. - -`make distclean` can be used for a complete cleanup of all artefacts. - -## Run - -`cargo run` can be used to start the Pokémon service on -`http://localhost:13734`. - -## Test - -`cargo test` can be used to spawn the service and run some simple integration -tests against it. - -More info can be found in the `tests` folder of `pokemon-service` package. - -## Benchmarks - -Please see [BENCHMARKS.md](/rust-runtime/aws-smithy-http-server/examples/BENCHMARKS.md). diff --git a/rust-runtime/aws-smithy-http-server/examples/pokemon-service/Cargo.toml b/rust-runtime/aws-smithy-http-server/examples/pokemon-service/Cargo.toml deleted file mode 100644 index a31c79d3e7..0000000000 --- a/rust-runtime/aws-smithy-http-server/examples/pokemon-service/Cargo.toml +++ /dev/null @@ -1,62 +0,0 @@ -[package] -name = "pokemon-service" -version = "0.1.0" -edition = "2021" -publish = false -authors = ["Smithy-rs Server Team "] -description = "A smithy Rust service to retrieve information about Pokémon." -default-run = "pokemon-service" - -[[bin]] -name = "pokemon-service" -path = "src/bin/pokemon-service.rs" - -[[bin]] -name = "pokemon-service-tls" -path = "src/bin/pokemon-service-tls.rs" - -[[bin]] -name = "pokemon-service-lambda" -path = "src/bin/pokemon-service-lambda.rs" - -[[bin]] -name = "pokemon-service-connect-info" -path = "src/bin/pokemon-service-connect-info.rs" - -[dependencies] -async-stream = "0.3" -clap = { version = "~3.2.1", features = ["derive"] } -hyper = {version = "0.14.12", features = ["server"] } -rand = "0.8" -tokio = "1.20.1" -tower = "0.4" -tower-http = { version = "0.3", features = ["trace"] } -tracing = "0.1" -tracing-subscriber = { version = "0.3.15", features = ["env-filter", "json"] } - -# These dependencies are only required for the `pokemon-service-tls` program. -tls-listener = { version = "0.5.1", features = ["rustls", "hyper-h2"] } -tokio-rustls = "0.23.4" -rustls-pemfile = "1.0.1" -futures-util = { version = "0.3.16", default-features = false } - -# This dependency is only required for the `pokemon-service-lambda` program. -lambda_http = "0.7.1" - -# Local paths -aws-smithy-http-server = { path = "../../", features = ["aws-lambda", "request-id"] } -pokemon-service-server-sdk = { path = "../pokemon-service-server-sdk/" } - -[dev-dependencies] -assert_cmd = "2.0" -home = "0.5" -serial_test = "0.7.0" - -# This dependency is only required for testing the `pokemon-service-tls` program. -hyper-rustls = { version = "0.23.0", features = ["http2"] } - -# Local paths -aws-smithy-client = { path = "../../../aws-smithy-client/", features = ["rustls"] } -aws-smithy-http = { path = "../../../aws-smithy-http/" } -aws-smithy-types = { path = "../../../aws-smithy-types/" } -pokemon-service-client = { path = "../pokemon-service-client/" } diff --git a/rust-runtime/aws-smithy-http-server/examples/pokemon-service/src/bin/pokemon-service-connect-info.rs b/rust-runtime/aws-smithy-http-server/examples/pokemon-service/src/bin/pokemon-service-connect-info.rs deleted file mode 100644 index 545b09a307..0000000000 --- a/rust-runtime/aws-smithy-http-server/examples/pokemon-service/src/bin/pokemon-service-connect-info.rs +++ /dev/null @@ -1,95 +0,0 @@ -/* - * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. - * SPDX-License-Identifier: Apache-2.0 - */ - -use std::net::{IpAddr, SocketAddr}; - -use aws_smithy_http_server::{ - request::connect_info::ConnectInfo, request::request_id::ServerRequestId, - request::request_id::ServerRequestIdProviderLayer, -}; -use clap::Parser; -use pokemon_service::{capture_pokemon, check_health, get_pokemon_species, get_server_statistics, setup_tracing}; -use pokemon_service_server_sdk::{ - error::{GetStorageError, StorageAccessNotAuthorized}, - input::{DoNothingInput, GetStorageInput}, - output::{DoNothingOutput, GetStorageOutput}, - PokemonService, -}; - -#[derive(Parser, Debug)] -#[clap(author, version, about, long_about = None)] -struct Args { - /// Hyper server bind address. - #[clap(short, long, action, default_value = "127.0.0.1")] - address: String, - /// Hyper server bind port. - #[clap(short, long, action, default_value = "13734")] - port: u16, -} - -/// Retrieves the user's storage. No authentication required for locals. -pub async fn get_storage_with_local_approved( - input: GetStorageInput, - connect_info: ConnectInfo, -) -> Result { - tracing::debug!("attempting to authenticate storage user"); - let local = connect_info.0.ip() == "127.0.0.1".parse::().unwrap(); - - // We currently support Ash: he has nothing stored - if input.user == "ash" && input.passcode == "pikachu123" { - return Ok(GetStorageOutput { collection: vec![] }); - } - // We support trainers in our gym - if local { - tracing::info!("welcome back"); - return Ok(GetStorageOutput { - collection: vec![ - String::from("bulbasaur"), - String::from("charmander"), - String::from("squirtle"), - ], - }); - } - tracing::debug!("authentication failed"); - Err(GetStorageError::StorageAccessNotAuthorized( - StorageAccessNotAuthorized {}, - )) -} - -pub async fn do_nothing_but_log_request_ids( - _input: DoNothingInput, - server_request_id: ServerRequestId, -) -> DoNothingOutput { - tracing::debug!("This request has this server ID: {}", server_request_id); - DoNothingOutput {} -} - -#[tokio::main] -async fn main() { - let args = Args::parse(); - setup_tracing(); - let app = PokemonService::builder_without_plugins() - .get_pokemon_species(get_pokemon_species) - .get_storage(get_storage_with_local_approved) - .get_server_statistics(get_server_statistics) - .capture_pokemon(capture_pokemon) - .do_nothing(do_nothing_but_log_request_ids) - .check_health(check_health) - .build() - .expect("failed to build an instance of PokemonService"); - - let app = app.layer(&ServerRequestIdProviderLayer::new()); - - // Start the [`hyper::Server`]. - let bind: SocketAddr = format!("{}:{}", args.address, args.port) - .parse() - .expect("unable to parse the server bind address and port"); - let server = hyper::Server::bind(&bind).serve(app.into_make_service_with_connect_info::()); - - // Run forever-ish... - if let Err(err) = server.await { - eprintln!("server error: {}", err); - } -} diff --git a/rust-runtime/aws-smithy-http-server/examples/pokemon-service/src/bin/pokemon-service-lambda.rs b/rust-runtime/aws-smithy-http-server/examples/pokemon-service/src/bin/pokemon-service-lambda.rs deleted file mode 100644 index 78b1bff621..0000000000 --- a/rust-runtime/aws-smithy-http-server/examples/pokemon-service/src/bin/pokemon-service-lambda.rs +++ /dev/null @@ -1,62 +0,0 @@ -/* - * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. - * SPDX-License-Identifier: Apache-2.0 - */ - -// This program is exported as a binary named `pokemon-service-lambda`. -use std::sync::Arc; - -use aws_smithy_http_server::{ - plugin::PluginPipeline, request::lambda::Context, routing::LambdaHandler, AddExtensionLayer, Extension, -}; -use pokemon_service::{ - capture_pokemon, check_health, do_nothing, get_pokemon_species, get_server_statistics, plugin::PrintExt, - setup_tracing, State, -}; -use pokemon_service_server_sdk::{error, input, output, PokemonService}; - -/// Retrieves the user's storage and records the . -pub async fn get_storage_lambda( - input: input::GetStorageInput, - _state: Extension>, - context: Context, -) -> Result { - tracing::debug!(request_id = %context.request_id, "attempting to authenticate storage user"); - - // We currently only support Ash and he has nothing stored - if !(input.user == "ash" && input.passcode == "pikachu123") { - tracing::debug!("authentication failed"); - return Err(error::GetStorageError::StorageAccessNotAuthorized( - error::StorageAccessNotAuthorized {}, - )); - } - Ok(output::GetStorageOutput { collection: vec![] }) -} - -#[tokio::main] -pub async fn main() { - setup_tracing(); - // Apply the `PrintPlugin` defined in `plugin.rs` - let plugins = PluginPipeline::new().print(); - let app = PokemonService::builder_with_plugins(plugins) - // Build a registry containing implementations to all the operations in the service. These - // are async functions or async closures that take as input the operation's input and - // return the operation's output. - .get_pokemon_species(get_pokemon_species) - .get_storage(get_storage_lambda) - .get_server_statistics(get_server_statistics) - .capture_pokemon(capture_pokemon) - .do_nothing(do_nothing) - .check_health(check_health) - .build() - .expect("failed to build an instance of PokemonService") - // Set up shared state and middlewares. - .layer(&AddExtensionLayer::new(Arc::new(State::default()))); - - let handler = LambdaHandler::new(app); - let lambda = lambda_http::run(handler); - - if let Err(err) = lambda.await { - eprintln!("lambda error: {}", err); - } -} diff --git a/rust-runtime/aws-smithy-http-server/examples/pokemon-service/tests/helpers.rs b/rust-runtime/aws-smithy-http-server/examples/pokemon-service/tests/helpers.rs deleted file mode 100644 index 1d632a27ed..0000000000 --- a/rust-runtime/aws-smithy-http-server/examples/pokemon-service/tests/helpers.rs +++ /dev/null @@ -1,132 +0,0 @@ -/* - * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. - * SPDX-License-Identifier: Apache-2.0 - */ - -use std::fs::File; -use std::io::BufReader; -use std::process::{Child, Command}; -use std::time::Duration; - -use assert_cmd::prelude::*; -use aws_smithy_client::{erase::DynConnector, hyper_ext::Adapter}; -use aws_smithy_http::operation::Request; -use pokemon_service_client::{Builder, Client, Config}; -use tokio::time; - -const TEST_KEY: &str = concat!(env!("CARGO_MANIFEST_DIR"), "/tests/testdata/localhost.key"); -const TEST_CERT: &str = concat!(env!("CARGO_MANIFEST_DIR"), "/tests/testdata/localhost.crt"); - -enum PokemonServiceVariant { - Http, - Https, -} - -impl PokemonServiceVariant { - async fn run_process(&self) -> Child { - let process = match self { - PokemonServiceVariant::Http => Command::cargo_bin("pokemon-service").unwrap().spawn().unwrap(), - PokemonServiceVariant::Https => Command::cargo_bin("pokemon-service-tls") - .unwrap() - .args(["--tls-cert-path", TEST_CERT, "--tls-key-path", TEST_KEY]) - .spawn() - .unwrap(), - }; - - // Give PokémonService some time to start up. - time::sleep(Duration::from_millis(500)).await; - - process - } - - fn base_url(&self) -> String { - match self { - PokemonServiceVariant::Http => "http://localhost:13734".to_string(), - PokemonServiceVariant::Https => "https://localhost:13734".to_string(), - } - } -} - -pub(crate) struct PokemonService { - child_process: Child, -} - -impl PokemonService { - #[allow(dead_code)] - pub(crate) async fn run() -> Self { - Self { - child_process: PokemonServiceVariant::Http.run_process().await, - } - } - - #[allow(dead_code)] - pub(crate) async fn run_https() -> Self { - Self { - child_process: PokemonServiceVariant::Https.run_process().await, - } - } -} - -impl Drop for PokemonService { - fn drop(&mut self) { - self.child_process - .kill() - .expect("failed to kill Pokémon Service program") - } -} - -#[allow(dead_code)] -pub fn client() -> Client< - aws_smithy_client::erase::DynConnector, - aws_smithy_client::erase::DynMiddleware, -> { - let base_url = PokemonServiceVariant::Http.base_url(); - let raw_client = Builder::new() - .rustls_connector(Default::default()) - .middleware_fn(rewrite_base_url(base_url)) - .build_dyn(); - let config = Config::builder().build(); - Client::with_config(raw_client, config) -} - -// Returns a client that only talks through https and http2 connections. -// It is useful in testing whether our server can talk to http2. -#[allow(dead_code)] -pub fn client_http2_only() -> Client< - aws_smithy_client::erase::DynConnector, - aws_smithy_client::erase::DynMiddleware, -> { - // Create custom cert store and add our test certificate to prevent unknown cert issues. - let mut reader = BufReader::new(File::open(TEST_CERT).expect("could not open certificate")); - let certs = rustls_pemfile::certs(&mut reader).expect("could not parse certificate"); - let mut roots = tokio_rustls::rustls::RootCertStore::empty(); - roots.add_parsable_certificates(&certs); - - let connector = hyper_rustls::HttpsConnectorBuilder::new() - .with_tls_config( - tokio_rustls::rustls::ClientConfig::builder() - .with_safe_defaults() - .with_root_certificates(roots) - .with_no_client_auth(), - ) - .https_only() - .enable_http2() - .build(); - - let base_url = PokemonServiceVariant::Https.base_url(); - let raw_client = Builder::new() - .connector(DynConnector::new(Adapter::builder().build(connector))) - .middleware_fn(rewrite_base_url(base_url)) - .build_dyn(); - let config = Config::builder().build(); - Client::with_config(raw_client, config) -} - -fn rewrite_base_url(base_url: String) -> impl Fn(Request) -> Request + Clone { - move |mut req| { - let http_req = req.http_mut(); - let uri = format!("{base_url}{}", http_req.uri().path()); - *http_req.uri_mut() = uri.parse().unwrap(); - req - } -} diff --git a/rust-runtime/aws-smithy-http-server/src/request/connect_info.rs b/rust-runtime/aws-smithy-http-server/src/request/connect_info.rs index 9dd1788f82..7a48e70421 100644 --- a/rust-runtime/aws-smithy-http-server/src/request/connect_info.rs +++ b/rust-runtime/aws-smithy-http-server/src/request/connect_info.rs @@ -7,8 +7,8 @@ //! [`IntoMakeServiceWithConnectInfo`](crate::routing::IntoMakeServiceWithConnectInfo) is used. [`ConnectInfo`]'s //! [`FromParts`] implementation allows it to be extracted from the [`http::Request`]. //! -//! The [`pokemon-service-connect-info.rs`](https://github.com/awslabs/smithy-rs/blob/main/rust-runtime/aws-smithy-http-server/examples/pokemon-service/src/bin/pokemon-service-connect-info.rs) -//! example illustrates the use of [`IntoMakeServiceWithConnectInfo`](crate::routing::IntoMakeServiceWithConnectInfo) +//! The [`example service`](https://github.com/awslabs/smithy-rs/blob/main/examples/pokemon-service/src/main.rs) +//! illustrates the use of [`IntoMakeServiceWithConnectInfo`](crate::routing::IntoMakeServiceWithConnectInfo) //! and [`ConnectInfo`] with a service builder. use http::request::Parts; diff --git a/tools/ci-scripts/check-server-e2e-test b/tools/ci-scripts/check-server-e2e-test index 8b17890724..2178b3d74b 100755 --- a/tools/ci-scripts/check-server-e2e-test +++ b/tools/ci-scripts/check-server-e2e-test @@ -4,6 +4,6 @@ # SPDX-License-Identifier: Apache-2.0 set -eux -cd smithy-rs/rust-runtime/aws-smithy-http-server/examples +cd smithy-rs/examples make test clippy