Skip to content
This repository was archived by the owner on Jul 14, 2025. It is now read-only.
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "wagi"
version = "0.6.2"
version = "0.8.1"
authors = ["Matt Butcher <[email protected]>"]
edition = "2021"

Expand All @@ -17,6 +17,7 @@
hyper = { version = "0.14", features = ["full"] }
indexmap = { version = "^1.6.2", features = ["serde"] }
oci-distribution = "0.6"
reqwest = { version = "0.11", features = ["stream"] }
serde = { version = "1.0", features = ["derive"] }
sha2 = "0.9"
tokio = { version = "1.1", features = ["full"] }
Expand Down
4 changes: 3 additions & 1 deletion docs/configuring_and_running.md
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,8 @@ In a nutshell, these are the fields that `modules.toml` supports.
- `route` (REQUIRED): The path that is appended to the server URL to create a full URL (e.g. `/foo` becomes `https://example.com/foo`)
- `module` (REQUIRED): A module reference. See Module References below.
- `repository`: RESERVED for future use
- `entrypoint` (default: `_start`): The name of the function within the module. This will directly execute that function. Most WASM/WASI implementations create a `_start` function by default. An example of a module that declares 3 entrypoints can be found [here](https://github.com/technosophos/hello-wagi).
- `entrypoint` (Optional, default: `_start`): The name of the function within the module. This will directly execute that function. Most WASM/WASI implementations create a `_start` function by default. An example of a module that declares 3 entrypoints can be found [here](https://github.com/technosophos/hello-wagi).
- `argv`: (Optional, default: "${SCRIPT_NAME} ${ARGS}"). This determines what the `argv` array looks like for the invoked program. The CGI 1.1 spec says that the `argv` array should contain the script name followed by the parameters. However, some Wasm modules require specifically formatted `argv`. This allows a way to override the CGI 1.1 defaults. Example: `argv = "ruby index.rb ${SCRIPT_NAME} ${ARGS}"`. This could expand to `ruby index.rb /example param1=val1 param2=val2`

Here is a brief example of a `modules.toml` file that declares two routes:

Expand Down Expand Up @@ -262,6 +263,7 @@ The following features are available for Wagi under `feature.wagi.FEATURE`:
| route | The relative path from the server route. e.g. "/foo" is mapped to http://example.com/foo |
| allowed_hosts | A comma-separated list of hosts that the HTTP client is allowed to access |
| file | If this is "true", this parcel will be treated as a file for consumption by a Wagi module |
| argv | If this is set, use this as a template for building the `argv` array. Two values are substituted: `${SCRIPT_NAME}` is replaced with the CGI `$SCRIPT_NAME` and `${ARGS}` is replaced with the query parameters formatted for CGI. |

### Simple Bindle Example

Expand Down
6 changes: 3 additions & 3 deletions docs/installation.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,8 @@ and download the desired release. Usually, the most recent release is the one yo
You can generate and compare the SHA with `shasum`:

```console
$ shasum wagi-v0.6.2-linux-amd64.tar.gz
ad4114b2ed9e510a8c24348d5ea544da55c685f5 wagi-v0.6.2-linux-amd64.tar.gz
$ shasum wagi-v0.8.1-linux-amd64.tar.gz
ad4114b2ed9e510a8c24348d5ea544da55c685f5 wagi-v0.8.1-linux-amd64.tar.gz
```

You can then compare that SHA with the one present in the release notes.
Expand Down Expand Up @@ -40,7 +40,7 @@ To build a static binary, run the following command:

```console
$ make build
Compiling wagi v0.6.2 (/Users/technosophos/Code/Rust/wagi)
Compiling wagi v0.8.1 (/Users/technosophos/Code/Rust/wagi)
Finished release [optimized] target(s) in 18.47s
```

Expand Down
159 changes: 132 additions & 27 deletions src/bindle_util.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
use std::{collections::{HashMap, HashSet}, iter::FromIterator};
use std::{
collections::{HashMap, HashSet},
iter::FromIterator,
};

use bindle::{Invoice, Parcel};

Expand Down Expand Up @@ -45,30 +48,35 @@ impl InvoiceUnderstander {
pub fn classify_parcel(&self, parcel: &Parcel) -> Option<InterestingParcel> {
// Currently only handlers but we have talked of scheduled tasks etc.
parcel.label.feature.as_ref().and_then(|features| {
features.get("wagi").and_then(|wagi_features| {
match wagi_features.get("route") {
features
.get("wagi")
.and_then(|wagi_features| match wagi_features.get("route") {
Some(route) => {
let handler_info = WagiHandlerInfo {
invoice_id: self.id(),
parcel: parcel.clone(),
route: route.to_owned(),
entrypoint: wagi_features.get("entrypoint").map(|s| s.to_owned()),
allowed_hosts: wagi_features.get("allowed_hosts").map(|h| parse_csv(h)),
required_parcels: parcels_required_for(parcel, &self.group_dependency_map),
argv: wagi_features.get("argv").map(|s| s.to_owned()),
required_parcels: parcels_required_for(
parcel,
&self.group_dependency_map,
),
};
Some(InterestingParcel::WagiHandler(handler_info))
},
}
None => None,
}
})
})
})
}

pub fn parse_wagi_handlers(&self) -> Vec<WagiHandlerInfo> {
self
.top_modules().iter()
self.top_modules()
.iter()
.filter_map(|parcel| self.classify_parcel(parcel))
.map(|parcel| match parcel { // If there are other cases of InterestingParcel this may need to become a filter_map, but right now that makes Clippy mad
.map(|parcel| match parcel {
// If there are other cases of InterestingParcel this may need to become a filter_map, but right now that makes Clippy mad
InterestingParcel::WagiHandler(h) => h,
})
.collect()
Expand All @@ -87,31 +95,50 @@ pub struct WagiHandlerInfo {
pub entrypoint: Option<String>,
pub allowed_hosts: Option<Vec<String>>,
pub required_parcels: Vec<Parcel>,
pub argv: Option<String>,
}

impl WagiHandlerInfo {
pub fn asset_parcels(&self) -> Vec<Parcel> {
self.required_parcels.iter().filter(|p| is_file(p)).cloned().collect()
self.required_parcels
.iter()
.filter(|p| is_file(p))
.cloned()
.collect()
}
}

const NO_PARCELS: Vec<Parcel> = vec![];

pub fn is_file(parcel: &Parcel) -> bool {
parcel.label.feature.as_ref().and_then(|features| {
features.get("wagi").map(|wagi_features| {
match wagi_features.get("file") {
Some(s) => s == "true",
_ => false,
}
parcel
.label
.feature
.as_ref()
.and_then(|features| {
features
.get("wagi")
.map(|wagi_features| match wagi_features.get("file") {
Some(s) => s == "true",
_ => false,
})
})
}).unwrap_or(false)
.unwrap_or(false)
}

pub fn parcels_required_for(parcel: &Parcel, full_dep_map: &HashMap<String, Vec<Parcel>>) -> Vec<Parcel> {
pub fn parcels_required_for(
parcel: &Parcel,
full_dep_map: &HashMap<String, Vec<Parcel>>,
) -> Vec<Parcel> {
let mut required = HashSet::new();
for group in parcel.directly_requires() {
required.extend(full_dep_map.get(&group).unwrap_or(&NO_PARCELS).iter().cloned());
required.extend(
full_dep_map
.get(&group)
.unwrap_or(&NO_PARCELS)
.iter()
.cloned(),
);
}
Vec::from_iter(required)
}
Expand Down Expand Up @@ -142,25 +169,39 @@ pub fn build_full_memberships(invoice: &Invoice) -> HashMap<String, Vec<Parcel>>
for group in direct_memberships.keys() {
let mut all_members = HashSet::new();
for dep_group in gg_deps.get(group).unwrap() {
all_members.extend(direct_memberships.get(dep_group).unwrap_or(&NO_PARCELS).iter().cloned());
all_members.extend(
direct_memberships
.get(dep_group)
.unwrap_or(&NO_PARCELS)
.iter()
.cloned(),
);
}
full_memberships.insert(group.to_owned(), Vec::from_iter(all_members));
}

full_memberships
}

fn group_to_group_direct_dependencies(direct_memberships: &HashMap<String, Vec<Parcel>>) -> HashMap<String, Vec<String>> {
fn group_to_group_direct_dependencies(
direct_memberships: &HashMap<String, Vec<Parcel>>,
) -> HashMap<String, Vec<String>> {
let mut ggd = HashMap::new();
for (group, members) in direct_memberships {
let mut directs: Vec<_> = members.iter().flat_map(|parcel| parcel.directly_requires()).collect();
let mut directs: Vec<_> = members
.iter()
.flat_map(|parcel| parcel.directly_requires())
.collect();
directs.push(group.to_owned());
ggd.insert(group.to_owned(), directs);
}
ggd
}

fn direct_deps_not_already_in_list(list: &[String], direct_dep_map: &HashMap<String, Vec<String>>) -> Vec<String> {
fn direct_deps_not_already_in_list(
list: &[String],
direct_dep_map: &HashMap<String, Vec<String>>,
) -> Vec<String> {
let mut new_dds = vec![];
for group in list {
if let Some(child_groups) = direct_dep_map.get(group) {
Expand All @@ -172,7 +213,9 @@ fn direct_deps_not_already_in_list(list: &[String], direct_dep_map: &HashMap<Str
HashSet::<String>::from_iter(new_dds).into_iter().collect()
}

fn group_to_group_full_dependencies(direct_memberships: &HashMap<String, Vec<Parcel>>) -> HashMap<String, Vec<String>> {
fn group_to_group_full_dependencies(
direct_memberships: &HashMap<String, Vec<Parcel>>,
) -> HashMap<String, Vec<String>> {
let mut ggd = HashMap::new();
let direct_deps = group_to_group_direct_dependencies(direct_memberships);
for (group, directs) in &direct_deps {
Expand Down Expand Up @@ -208,7 +251,67 @@ impl ParcelUtils for Parcel {
}

fn parse_csv(text: &str) -> Vec<String> {
text.split(',').map(|v| v.to_owned()).collect() // TODO: trim etc.?
text.split(',').map(|v| v.to_owned()).collect() // TODO: trim etc.?
}

// Bindle client/auth utils, derived from github.com/deislabs/hippo-cli

use std::sync::Arc;

use bindle::client::{
tokens::{HttpBasic, NoToken, TokenManager},
Client, ClientBuilder,
};

#[derive(Clone)]
pub struct BindleConnectionInfo {
base_url: String,
allow_insecure: bool,
token_manager: AnyAuth,
}

impl BindleConnectionInfo {
pub fn new<I: Into<String>>(
base_url: I,
allow_insecure: bool,
username: Option<String>,
password: Option<String>,
) -> Self {
let token_manager: Box<dyn TokenManager + Send + Sync> = match (username, password) {
(Some(u), Some(p)) => Box::new(HttpBasic::new(&u, &p)),
_ => Box::new(NoToken::default()),
};

Self {
base_url: base_url.into(),
allow_insecure,
token_manager: AnyAuth {
token_manager: Arc::new(token_manager),
},
}
}

pub fn client(&self) -> bindle::client::Result<Client<AnyAuth>> {
let builder = ClientBuilder::default()
.http2_prior_knowledge(false)
.danger_accept_invalid_certs(self.allow_insecure);
builder.build(&self.base_url, self.token_manager.clone())
}
}

#[derive(Clone)]
pub struct AnyAuth {
token_manager: Arc<Box<dyn TokenManager + Send + Sync>>,
}

#[async_trait::async_trait]
impl TokenManager for AnyAuth {
async fn apply_auth_header(
&self,
builder: reqwest::RequestBuilder,
) -> bindle::client::Result<reqwest::RequestBuilder> {
self.token_manager.apply_auth_header(builder).await
}
}

#[cfg(test)]
Expand Down Expand Up @@ -394,7 +497,9 @@ mod test {
};

let membership_map = build_full_memberships(&inv);
let members = membership_map.get("coffee").expect("there should have been a group called 'coffee'");
let members = membership_map
.get("coffee")
.expect("there should have been a group called 'coffee'");
assert_eq!(2, members.len());
}
}
Loading