Skip to content

Commit ba1fd63

Browse files
authored
add wstd-aws crate, for integration with aws-sdk (#102)
* wstd::http::client: derive Clone * add wstd-aws crate, for using the aws sdk on top of wstd * wstd-sdk: add s3 bucket listing and fetch as an example derived from https://github.com/awsdocs/aws-doc-sdk-examples/blob/main/rustv1/examples/s3/src/bin/s3-helloworld.rs#L35 * ci: assume wstd-aws-ci-role * test runs aws s3 example * add wstd-aws to publish * ci: id-token write * typo * runner: forward session token as well * example: bugfix, clean up output * need to debug ci * does action put them into the env properly?? * remove debugging, add comments, ci can skip aws if auth fails * readmes * typo * comments runner capabilities
1 parent a8ee5e5 commit ba1fd63

File tree

13 files changed

+522
-74
lines changed

13 files changed

+522
-74
lines changed

.cargo/config.toml

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,5 @@
11
[target.wasm32-wasip2]
2-
runner = "wasmtime -Shttp"
2+
# wasmtime is given:
3+
# * AWS auth environment variables, for running the wstd-aws integration tests.
4+
# * . directory is available at .
5+
runner = "wasmtime run -Shttp --env AWS_ACCESS_KEY_ID --env AWS_SECRET_ACCESS_KEY --env AWS_SESSION_TOKEN --dir .::."

.github/workflows/ci.yaml

Lines changed: 28 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,10 @@ on:
1010
env:
1111
RUSTFLAGS: -Dwarnings
1212

13+
# required for AWS oidc
14+
permissions:
15+
id-token: write
16+
1317
jobs:
1418
build_and_test:
1519
name: Build and test
@@ -26,24 +30,35 @@ jobs:
2630
- name: Install wasmtime
2731
uses: bytecodealliance/actions/wasmtime/setup@v1
2832

29-
- name: check
30-
uses: actions-rs/cargo@v1
33+
# pchickey made a role `wstd-aws-ci-role` and bucket `wstd-example-bucket`
34+
# on his personal aws account 313377415443. The role only provides
35+
# ListBucket and GetObject for the example bucket, which is enough to pass
36+
# the single integration test. The role is configured to trust GitHub
37+
# actions for the bytecodealliance/wstd repo. This action will set the
38+
# AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY, and AWS_SESSION_TOKEN
39+
# environment variables.
40+
- name: get aws credentials
41+
id: creds
42+
uses: aws-actions/[email protected]
43+
continue-on-error: true
3144
with:
32-
command: check
33-
args: --workspace --all --bins --examples
45+
aws-region: us-west-2
46+
role-to-assume: arn:aws:iam::313377415443:role/wstd-aws-ci-role
47+
role-session-name: github-ci
48+
49+
- name: check
50+
run: cargo check --workspace --all --bins --examples
3451

3552
- name: wstd tests
36-
uses: actions-rs/cargo@v1
37-
with:
38-
command: test
39-
args: -p wstd --target wasm32-wasip2 -- --nocapture
53+
run: cargo test -p wstd -p wstd-axum -p wstd-aws --target wasm32-wasip2 -- --nocapture
4054

41-
- name: example tests
42-
uses: actions-rs/cargo@v1
43-
with:
44-
command: test
45-
args: -p test-programs -- --nocapture
55+
- name: test-programs tests
56+
run: cargo test -p test-programs -- --nocapture
57+
if: steps.creds.outcome == 'success'
4658

59+
- name: test-programs tests (no aws)
60+
run: cargo test -p test-programs --features no-aws -- --nocapture
61+
if: steps.creds.outcome != 'success'
4762

4863
check_fmt_and_docs:
4964
name: Checking fmt and docs

Cargo.toml

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ serde = { workspace = true, features = ["derive"] }
4444
serde_json.workspace = true
4545

4646
[workspace]
47-
members = [
47+
members = ["aws",
4848
"axum",
4949
"axum/macro",
5050
"macro",
@@ -70,6 +70,11 @@ authors = [
7070
[workspace.dependencies]
7171
anyhow = "1"
7272
async-task = "4.7"
73+
aws-config = { version = "1.8.8", default-features = false }
74+
aws-sdk-s3 = { version = "1.108.0", default-features = false }
75+
aws-smithy-async = { version = "1.2.6", default-features = false }
76+
aws-smithy-types = { version = "1.3.3", default-features = false }
77+
aws-smithy-runtime-api = { version = "1.9.1", default-features = false }
7378
axum = { version = "0.8.6", default-features = false }
7479
bytes = "1.10.1"
7580
cargo_metadata = "0.22"
@@ -88,6 +93,7 @@ quote = "1.0"
8893
serde= "1"
8994
serde_json = "1"
9095
serde_qs = "0.15"
96+
sync_wrapper = "1"
9197
slab = "0.4.9"
9298
syn = "2.0"
9399
test-log = { version = "0.2", features = ["trace"] }

aws/.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
.environment

aws/Cargo.toml

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
[package]
2+
name = "wstd-aws"
3+
description = ""
4+
version.workspace = true
5+
edition.workspace = true
6+
license.workspace = true
7+
repository.workspace = true
8+
keywords.workspace = true
9+
categories.workspace = true
10+
rust-version.workspace = true
11+
authors.workspace = true
12+
13+
[dependencies]
14+
anyhow.workspace = true
15+
aws-smithy-async = { workspace = true }
16+
aws-smithy-types = { workspace = true, features = ["http-body-1-x"] }
17+
aws-smithy-runtime-api = { workspace = true, features = ["client", "http-1x"] }
18+
http-body-util.workspace = true
19+
sync_wrapper = { workspace = true, features = ["futures"] }
20+
wstd.workspace = true
21+
22+
[dev-dependencies]
23+
aws-config.workspace = true
24+
aws-sdk-s3.workspace = true
25+
clap.workspace = true

aws/README.md

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
2+
# wstd-aws: wstd support for the AWS Rust SDK
3+
4+
This crate provides support for using the AWS Rust SDK for the `wasm32-wasip2`
5+
target using the [`wstd`] crate.
6+
7+
In many wasi settings, its necessary or desirable to use the wasi-http
8+
interface to make http requests. Wasi-http interfaces provide an http
9+
implementation, including the sockets layer and TLS, outside of the user's
10+
component. `wstd` provides user-friendly async Rust interfaces to all of the
11+
standardized wasi interfaces, including wasi-http.
12+
13+
The AWS Rust SDK, by default, depends on `tokio`, `hyper`, and either `rustls`
14+
or `s2n_tls`, and makes http requests over sockets (which can be provided as
15+
wasi-sockets). Those dependencies may not work correctly under `wasm32-wasip2`,
16+
and if they do, they will not use the wasi-http interfaces. To avoid using
17+
http over sockets, make sure to set the `default-features = false` setting
18+
when depending on any `aws-*` crates in your project.
19+
20+
To configure `wstd`'s wasi-http client for the AWS Rust SDK, provide
21+
`wstd_aws::sleep_impl()` and `wstd_aws::http_client()` to your
22+
[`aws_config::ConfigLoader`]:
23+
24+
```
25+
let config = aws_config::defaults(BehaviorVersion::latest())
26+
.sleep_impl(wstd_aws::sleep_impl())
27+
.http_client(wstd_aws::http_client())
28+
...;
29+
```
30+
31+
[`wstd`]: https://docs.rs/wstd/latest/wstd
32+
[`aws_config::ConfigLoader`]: https://docs.rs/aws-config/1.8.8/aws_config/struct.ConfigLoader.html
33+
34+
## Example
35+
36+
An example s3 client is provided as a wasi cli command. It accepts command
37+
line arguments with the subcommand `list` to list a bucket's contents, and
38+
`get <key>` to get an object from a bucket and write it to the filesystem.
39+
40+
This example *must be compiled in release mode* - in debug mode, the aws
41+
sdk's generated code will overflow the maximum permitted wasm locals in
42+
a single function.
43+
44+
Compile it with:
45+
46+
```sh
47+
cargo build -p wstd-aws --target wasm32-wasip2 --release --examples
48+
```
49+
50+
When running this example, you will need AWS credentials provided in environment
51+
variables.
52+
53+
Run it with:
54+
```sh
55+
wasmtime run -Shttp \
56+
--env AWS_ACCESS_KEY_ID \
57+
--env AWS_SECRET_ACCESS_KEY \
58+
--env AWS_SESSION_TOKEN \
59+
--dir .::. \
60+
target/wasm32-wasip2/release/examples/s3.wasm
61+
```
62+
63+
or alternatively run it with:
64+
```sh
65+
cargo run --target wasm32-wasip2 -p wstd-aws --example s3
66+
```
67+
68+
which uses the wasmtime cli, as above, via configiration found in this
69+
workspace's `.cargo/config`.
70+
71+
By default, this script accesses the `wstd-example-bucket` in `us-west-2`.
72+
To change the bucket or region, use the `--bucket` and `--region` cli
73+
flags before the subcommand.
74+
75+

aws/examples/s3.rs

Lines changed: 149 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,149 @@
1+
//! Example s3 client running on `wstd` via `wstd_aws`
2+
//!
3+
//! This example is a wasi cli command. It accepts command line arguments
4+
//! with the subcommand `list` to list a bucket's contents, and `get <key>`
5+
//! to get an object from a bucket and write it to the filesystem.
6+
//!
7+
//! This example *must be compiled in release mode* - in debug mode, the aws
8+
//! sdk's generated code will overflow the maximum permitted wasm locals in
9+
//! a single function.
10+
//!
11+
//! Compile it with:
12+
//!
13+
//! ```sh
14+
//! cargo build -p wstd-aws --target wasm32-wasip2 --release --examples
15+
//! ```
16+
//!
17+
//! When running this example, you will need AWS credentials provided in environment
18+
//! variables.
19+
//!
20+
//! Run it with:
21+
//! ```sh
22+
//! wasmtime run -Shttp \
23+
//! --env AWS_ACCESS_KEY_ID \
24+
//! --env AWS_SECRET_ACCESS_KEY \
25+
//! --env AWS_SESSION_TOKEN \
26+
//! --dir .::. \
27+
//! target/wasm22-wasip2/release/examples/s3.wasm
28+
//! ```
29+
//!
30+
//! or alternatively run it with:
31+
//! ```sh
32+
//! cargo run --target wasm32-wasip2 -p wstd-aws --example s3
33+
//! ```
34+
//!
35+
//! which uses the wasmtime cli, as above, via configiration found in this
36+
//! workspace's `.cargo/config`.
37+
//!
38+
//! By default, this script accesses the `wstd-example-bucket` in `us-west-2`.
39+
//! To change the bucket or region, use the `--bucket` and `--region` cli
40+
//! flags before the subcommand.
41+
42+
use anyhow::Result;
43+
use clap::{Parser, Subcommand};
44+
45+
use aws_config::{BehaviorVersion, Region};
46+
use aws_sdk_s3::Client;
47+
48+
#[derive(Debug, Parser)]
49+
#[command(version, about, long_about = None)]
50+
struct Opts {
51+
/// The AWS Region. Defaults to us-west-2 if not provided.
52+
#[arg(short, long)]
53+
region: Option<String>,
54+
/// The name of the bucket. Defaults to wstd-example-bucket if not
55+
/// provided.
56+
#[arg(short, long)]
57+
bucket: Option<String>,
58+
59+
#[command(subcommand)]
60+
command: Option<Command>,
61+
}
62+
63+
#[derive(Subcommand, Debug)]
64+
enum Command {
65+
List,
66+
Get {
67+
key: String,
68+
#[arg(short, long)]
69+
out: Option<String>,
70+
},
71+
}
72+
73+
#[wstd::main]
74+
async fn main() -> Result<()> {
75+
let opts = Opts::parse();
76+
let region = opts
77+
.region
78+
.clone()
79+
.unwrap_or_else(|| "us-west-2".to_owned());
80+
let bucket = opts
81+
.bucket
82+
.clone()
83+
.unwrap_or_else(|| "wstd-example-bucket".to_owned());
84+
85+
let config = aws_config::defaults(BehaviorVersion::latest())
86+
.region(Region::new(region))
87+
.sleep_impl(wstd_aws::sleep_impl())
88+
.http_client(wstd_aws::http_client())
89+
.load()
90+
.await;
91+
92+
let client = Client::new(&config);
93+
94+
match opts.command.as_ref().unwrap_or(&Command::List) {
95+
Command::List => {
96+
let output = list(&bucket, &client).await?;
97+
print!("{}", output);
98+
}
99+
Command::Get { key, out } => {
100+
let contents = get(&bucket, &client, key).await?;
101+
let output: &str = if let Some(out) = out {
102+
out.as_str()
103+
} else {
104+
key.as_str()
105+
};
106+
std::fs::write(output, contents)?;
107+
}
108+
}
109+
Ok(())
110+
}
111+
112+
async fn list(bucket: &str, client: &Client) -> Result<String> {
113+
let mut listing = client
114+
.list_objects_v2()
115+
.bucket(bucket.to_owned())
116+
.into_paginator()
117+
.send();
118+
119+
let mut output = String::new();
120+
output += "key\tetag\tlast_modified\tstorage_class\n";
121+
while let Some(res) = listing.next().await {
122+
let object = res?;
123+
for item in object.contents() {
124+
output += &format!(
125+
"{}\t{}\t{}\t{}\n",
126+
item.key().unwrap_or_default(),
127+
item.e_tag().unwrap_or_default(),
128+
item.last_modified()
129+
.map(|lm| format!("{lm}"))
130+
.unwrap_or_default(),
131+
item.storage_class()
132+
.map(|sc| format!("{sc}"))
133+
.unwrap_or_default(),
134+
);
135+
}
136+
}
137+
Ok(output)
138+
}
139+
140+
async fn get(bucket: &str, client: &Client, key: &str) -> Result<Vec<u8>> {
141+
let object = client
142+
.get_object()
143+
.bucket(bucket.to_owned())
144+
.key(key)
145+
.send()
146+
.await?;
147+
let data = object.body.collect().await?;
148+
Ok(data.to_vec())
149+
}

0 commit comments

Comments
 (0)