mirror of https://github.com/smithy-lang/smithy-rs
Move examples to root, refactor to workspace, and refactor integration tests (#2481)
* Move examples * Update documentation * Add to CI * Fix CI * Cleanup * Fix clippy lints * Fix documentation * Bump example dependencies * Cleanup * Update documentation --------- Co-authored-by: Matteo Bigoi <1781140+crisidev@users.noreply.github.com>
This commit is contained in:
parent
f3e44742e3
commit
a737694f73
|
@ -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
|
||||
-------
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -1,2 +1,3 @@
|
|||
pokemon-service-client/
|
||||
pokemon-service-server-sdk/
|
||||
Cargo.lock
|
|
@ -5,11 +5,12 @@ using [wrk](https://github.com/wg/wrk).
|
|||
|
||||
<!-- vim-markdown-toc Marked -->
|
||||
|
||||
* [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)
|
||||
|
||||
<!-- vim-markdown-toc -->
|
||||
|
||||
|
@ -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
|
|
@ -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]
|
|
@ -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).
|
|
@ -0,0 +1,24 @@
|
|||
[package]
|
||||
name = "pokemon-service-common"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
publish = false
|
||||
authors = ["Smithy-rs Server Team <smithy-rs-server@amazon.com>"]
|
||||
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"
|
|
@ -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<Arc<State>>,
|
||||
) -> Result<output::GetPokemonSpeciesOutput, error::GetPokemonSpeciesError> {
|
||||
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<output::CapturePokemonOutput, error::CapturePokemonError> {
|
||||
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;
|
|
@ -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<SdkBody>;
|
||||
|
@ -59,7 +64,10 @@ struct SentinelPlugin {
|
|||
|
||||
impl SentinelPlugin {
|
||||
pub fn new(name: &'static str, output: Arc<Mutex<Vec<&'static str>>>) -> Self {
|
||||
Self { name, output: output }
|
||||
Self {
|
||||
name,
|
||||
output: output,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,21 @@
|
|||
[package]
|
||||
name = "pokemon-service-lambda"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
publish = false
|
||||
authors = ["Smithy-rs Server Team <smithy-rs-server@amazon.com>"]
|
||||
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/" }
|
|
@ -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<Arc<State>>,
|
||||
context: Context,
|
||||
) -> Result<GetStorageOutput, GetStorageError> {
|
||||
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![] })
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,36 @@
|
|||
[package]
|
||||
name = "pokemon-service-tls"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
publish = false
|
||||
authors = ["Smithy-rs Server Team <smithy-rs-server@amazon.com>"]
|
||||
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/" }
|
|
@ -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";
|
|
@ -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);
|
||||
}
|
|
@ -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<DynConnector, DynMiddleware<DynConnector>> {
|
||||
// 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)
|
||||
}
|
|
@ -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();
|
||||
}
|
|
@ -0,0 +1,34 @@
|
|||
[package]
|
||||
name = "pokemon-service"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
publish = false
|
||||
authors = ["Smithy-rs Server Team <smithy-rs-server@amazon.com>"]
|
||||
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/" }
|
|
@ -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<SocketAddr>,
|
||||
) -> Result<GetStorageOutput, GetStorageError> {
|
||||
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::<IpAddr>().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")],
|
||||
})
|
||||
}
|
|
@ -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::<SocketAddr>();
|
||||
|
||||
// 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 {
|
|
@ -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<DynConnector, DynMiddleware<DynConnector>> {
|
||||
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)
|
||||
}
|
|
@ -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 {
|
|
@ -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());
|
||||
}
|
|
@ -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).
|
|
@ -1,62 +0,0 @@
|
|||
[package]
|
||||
name = "pokemon-service"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
publish = false
|
||||
authors = ["Smithy-rs Server Team <smithy-rs-server@amazon.com>"]
|
||||
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/" }
|
|
@ -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<SocketAddr>,
|
||||
) -> Result<GetStorageOutput, GetStorageError> {
|
||||
tracing::debug!("attempting to authenticate storage user");
|
||||
let local = connect_info.0.ip() == "127.0.0.1".parse::<IpAddr>().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::<SocketAddr>());
|
||||
|
||||
// Run forever-ish...
|
||||
if let Err(err) = server.await {
|
||||
eprintln!("server error: {}", err);
|
||||
}
|
||||
}
|
|
@ -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<Arc<State>>,
|
||||
context: Context,
|
||||
) -> Result<output::GetStorageOutput, error::GetStorageError> {
|
||||
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);
|
||||
}
|
||||
}
|
|
@ -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<aws_smithy_client::erase::DynConnector>,
|
||||
> {
|
||||
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<aws_smithy_client::erase::DynConnector>,
|
||||
> {
|
||||
// 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
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in New Issue