Skip to content

Commit

Permalink
Implement wasi-config
Browse files Browse the repository at this point in the history
Signed-off-by: itowlson <ivan.towlson@fermyon.com>
  • Loading branch information
itowlson committed Oct 7, 2024
1 parent 967fdf3 commit b20d169
Show file tree
Hide file tree
Showing 15 changed files with 183 additions and 1 deletion.
1 change: 1 addition & 0 deletions Cargo.lock

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

1 change: 1 addition & 0 deletions crates/expressions/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ edition = { workspace = true }
[dependencies]
anyhow = { workspace = true }
async-trait = { workspace = true }
futures = { workspace = true }
serde = { workspace = true }
spin-locked-app = { path = "../locked-app" }
thiserror = { workspace = true }
Expand Down
15 changes: 15 additions & 0 deletions crates/expressions/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,21 @@ impl ProviderResolver {
self.resolve_template(template).await
}

pub async fn resolve_all(&self, component_id: &str) -> Result<Vec<(String, String)>> {
use futures::FutureExt;

let Some(keys2templates) = self.internal.component_configs.get(component_id) else {
return Ok(vec![]);
};

let resolve_futs = keys2templates.iter().map(|(key, template)| {
self.resolve_template(template)
.map(|r| r.map(|value| (key.to_string(), value)))
});

futures::future::try_join_all(resolve_futs).await
}

/// Resolves the given template.
pub async fn resolve_template(&self, template: &Template) -> Result<String> {
let mut resolved_parts: Vec<Cow<str>> = Vec::with_capacity(template.parts().len());
Expand Down
43 changes: 42 additions & 1 deletion crates/factor-variables/src/host.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
use spin_factors::anyhow;
use spin_world::{async_trait, v1, v2::variables};
use spin_world::{async_trait, v1, v2::variables, wasi::config as wasi_config};
use tracing::{instrument, Level};

use crate::InstanceState;
Expand Down Expand Up @@ -37,6 +37,47 @@ impl v1::config::Host for InstanceState {
}
}

#[async_trait]
impl wasi_config::store::Host for InstanceState {
async fn get(&mut self, key: String) -> Result<Option<String>, wasi_config::store::Error> {
match <Self as variables::Host>::get(self, key).await {
Ok(value) => Ok(Some(value)),
Err(e) => {
match e {
variables::Error::Undefined(_) => Ok(None),
variables::Error::InvalidName(_) => Ok(None), // this is the guidance from https://github.com/WebAssembly/wasi-runtime-config/pull/19
variables::Error::Provider(msg) => {
Err(wasi_config::store::Error::Upstream(msg))
}
variables::Error::Other(msg) => Err(wasi_config::store::Error::Io(msg)),
}
}
}
}

async fn get_all(&mut self) -> Result<Vec<(String, String)>, wasi_config::store::Error> {
let all = self
.expression_resolver
.resolve_all(&self.component_id)
.await;
all.map_err(|e| {
match expressions_to_variables_err(e) {
variables::Error::Undefined(msg) => wasi_config::store::Error::Io(msg), // this shouldn't happen but just in case
variables::Error::InvalidName(msg) => wasi_config::store::Error::Io(msg), // this shouldn't happen but just in case
variables::Error::Provider(msg) => wasi_config::store::Error::Upstream(msg),
variables::Error::Other(msg) => wasi_config::store::Error::Io(msg),
}
})
}

fn convert_error(
&mut self,
err: wasi_config::store::Error,
) -> anyhow::Result<wasi_config::store::Error> {
Ok(err)
}
}

fn expressions_to_variables_err(err: spin_expressions::Error) -> variables::Error {
use spin_expressions::Error;
match err {
Expand Down
1 change: 1 addition & 0 deletions crates/factor-variables/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ impl Factor for VariablesFactor {
fn init<T: Send + 'static>(&mut self, mut ctx: InitContext<T, Self>) -> anyhow::Result<()> {
ctx.link_bindings(spin_world::v1::config::add_to_linker)?;
ctx.link_bindings(spin_world::v2::variables::add_to_linker)?;
ctx.link_bindings(spin_world::wasi::config::store::add_to_linker)?;
Ok(())
}

Expand Down
1 change: 1 addition & 0 deletions crates/world/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ wasmtime::component::bindgen!({
"fermyon:spin/sqlite@2.0.0/error" => v2::sqlite::Error,
"fermyon:spin/sqlite/error" => v1::sqlite::Error,
"fermyon:spin/variables@2.0.0/error" => v2::variables::Error,
"wasi:config/store/error" => wasi::config::store::Error,
},
trappable_imports: true,
});
Expand Down
17 changes: 17 additions & 0 deletions tests/runtime-tests/tests/variables/spin.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
spin_manifest_version = "1"
authors = [""]
description = ""
name = "variables"
trigger = { type = "http" }
version = "0.1.0"

[variables]
variable = { default = "value" }

[[component]]
id = "variables"
source = "%{source=variables}"
[component.trigger]
route = "/..."
[component.config]
variable = "{{ variable }}"
17 changes: 17 additions & 0 deletions tests/runtime-tests/tests/wasi-config/spin.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
spin_manifest_version = "1"
authors = [""]
description = ""
name = "wasi-config"
trigger = { type = "http" }
version = "0.1.0"

[variables]
variable = { default = "value" }

[[component]]
id = "wasi-config"
source = "%{source=wasi-config}"
[component.trigger]
route = "/..."
[component.config]
variable = "{{ variable }}"
8 changes: 8 additions & 0 deletions tests/test-components/components/Cargo.lock

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

11 changes: 11 additions & 0 deletions tests/test-components/components/wasi-config/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
[package]
name = "wasi-config"
version = "0.1.0"
edition = "2021"

[lib]
crate-type = ["cdylib"]

[dependencies]
helper = { path = "../../helper" }
wit-bindgen = "0.16.0"
8 changes: 8 additions & 0 deletions tests/test-components/components/wasi-config/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
# Variables

Tests the wasi:config interface.

## Expectations

This test component expects the following to be true:
* Only the variable named "variable" is defined with value "value"
23 changes: 23 additions & 0 deletions tests/test-components/components/wasi-config/src/lib.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
use helper::ensure_matches;

use bindings::wasi::config::store::{get, get_all};

helper::define_component!(Component);

impl Component {
fn main() -> Result<(), String> {
ensure_matches!(get("variable"), Ok(Some(val)) if val == "value");
ensure_matches!(get("non_existent"), Ok(None));

let expected_all = vec![
("variable".to_owned(), "value".to_owned()),
];
ensure_matches!(get_all(), Ok(val) if val == expected_all);

ensure_matches!(get("invalid-name"), Ok(None));
ensure_matches!(get("invalid!name"), Ok(None));
ensure_matches!(get("4invalidname"), Ok(None));

Ok(())
}
}
30 changes: 30 additions & 0 deletions wit/deps/wasi-runtime-config-2024-09-27/store.wit
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
interface store {
/// An error type that encapsulates the different errors that can occur fetching configuration values.
variant error {
/// This indicates an error from an "upstream" config source.
/// As this could be almost _anything_ (such as Vault, Kubernetes ConfigMaps, KeyValue buckets, etc),
/// the error message is a string.
upstream(string),
/// This indicates an error from an I/O operation.
/// As this could be almost _anything_ (such as a file read, network connection, etc),
/// the error message is a string.
/// Depending on how this ends up being consumed,
/// we may consider moving this to use the `wasi:io/error` type instead.
/// For simplicity right now in supporting multiple implementations, it is being left as a string.
io(string),
}

/// Gets a configuration value of type `string` associated with the `key`.
///
/// The value is returned as an `option<string>`. If the key is not found,
/// `Ok(none)` is returned. If an error occurs, an `Err(error)` is returned.
get: func(
/// A string key to fetch
key: string
) -> result<option<string>, error>;

/// Gets a list of configuration key-value pairs of type `string`.
///
/// If an error occurs, an `Err(error)` is returned.
get-all: func() -> result<list<tuple<string, string>>, error>;
}
6 changes: 6 additions & 0 deletions wit/deps/wasi-runtime-config-2024-09-27/world.wit
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package wasi:config@0.2.0-draft;

world imports {
/// The interface for wasi:config/store
import store;
}
2 changes: 2 additions & 0 deletions wit/world.wit
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ world http-trigger-rc20231018 {
world platform {
include wasi:cli/imports@0.2.0;
import wasi:http/outgoing-handler@0.2.0;
import wasi:config/store@0.2.0-draft;
import llm;
import redis;
import mqtt;
Expand All @@ -30,6 +31,7 @@ world platform {
world platform-rc20231018 {
include wasi:cli/reactor@0.2.0-rc-2023-10-18;
import wasi:http/outgoing-handler@0.2.0-rc-2023-10-18;
import wasi:config/store@0.2.0-draft;
import llm;
import redis;
import mqtt;
Expand Down

0 comments on commit b20d169

Please sign in to comment.