diff --git a/Cargo.lock b/Cargo.lock index 0b9ad06f7f..cdc168815a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -7069,6 +7069,7 @@ version = "2.8.0-pre0" dependencies = [ "anyhow", "async-trait", + "futures", "spin-locked-app", "thiserror", "tokio", diff --git a/crates/expressions/Cargo.toml b/crates/expressions/Cargo.toml index cc416867ed..6c9749fb4d 100644 --- a/crates/expressions/Cargo.toml +++ b/crates/expressions/Cargo.toml @@ -7,6 +7,7 @@ edition = { workspace = true } [dependencies] anyhow = { workspace = true } async-trait = { workspace = true } +futures = { workspace = true } spin-locked-app = { path = "../locked-app" } thiserror = { workspace = true } diff --git a/crates/expressions/src/lib.rs b/crates/expressions/src/lib.rs index 350914b558..23be70b654 100644 --- a/crates/expressions/src/lib.rs +++ b/crates/expressions/src/lib.rs @@ -52,6 +52,22 @@ impl ProviderResolver { self.resolve_template(template).await } + /// Resolves all variables for the given component. + pub async fn resolve_all(&self, component_id: &str) -> Result> { + 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 { let mut resolved_parts: Vec> = Vec::with_capacity(template.parts().len()); diff --git a/crates/factor-variables/src/host.rs b/crates/factor-variables/src/host.rs index bc30fe0a79..5ab38b50bc 100644 --- a/crates/factor-variables/src/host.rs +++ b/crates/factor-variables/src/host.rs @@ -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; @@ -37,6 +37,41 @@ impl v1::config::Host for InstanceState { } } +#[async_trait] +impl wasi_config::store::Host for InstanceState { + async fn get(&mut self, key: String) -> Result, wasi_config::store::Error> { + match ::get(self, key).await { + Ok(value) => Ok(Some(value)), + Err(variables::Error::Undefined(_)) => Ok(None), + Err(variables::Error::InvalidName(_)) => Ok(None), // this is the guidance from https://github.com/WebAssembly/wasi-runtime-config/pull/19) + Err(variables::Error::Provider(msg)) => Err(wasi_config::store::Error::Upstream(msg)), + Err(variables::Error::Other(msg)) => Err(wasi_config::store::Error::Io(msg)), + } + } + + async fn get_all(&mut self) -> Result, 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 { + Ok(err) + } +} + fn expressions_to_variables_err(err: spin_expressions::Error) -> variables::Error { use spin_expressions::Error; match err { diff --git a/crates/factor-variables/src/lib.rs b/crates/factor-variables/src/lib.rs index ac5c7e626b..0983696094 100644 --- a/crates/factor-variables/src/lib.rs +++ b/crates/factor-variables/src/lib.rs @@ -31,6 +31,7 @@ impl Factor for VariablesFactor { fn init(&mut self, mut ctx: InitContext) -> 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(()) } diff --git a/crates/world/src/lib.rs b/crates/world/src/lib.rs index 0ecb57ee1d..676ec3e54a 100644 --- a/crates/world/src/lib.rs +++ b/crates/world/src/lib.rs @@ -28,6 +28,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@0.2.0-draft-2024-09-27/error" => wasi::config::store::Error, }, trappable_imports: true, }); diff --git a/tests/runtime-tests/tests/variables/spin.toml b/tests/runtime-tests/tests/variables/spin.toml new file mode 100644 index 0000000000..b63cd61ddd --- /dev/null +++ b/tests/runtime-tests/tests/variables/spin.toml @@ -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 }}" diff --git a/tests/runtime-tests/tests/wasi-config/spin.toml b/tests/runtime-tests/tests/wasi-config/spin.toml new file mode 100644 index 0000000000..2eee4f45e9 --- /dev/null +++ b/tests/runtime-tests/tests/wasi-config/spin.toml @@ -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 }}" diff --git a/tests/test-components/components/Cargo.lock b/tests/test-components/components/Cargo.lock index 3c6212dd85..bec40906b2 100644 --- a/tests/test-components/components/Cargo.lock +++ b/tests/test-components/components/Cargo.lock @@ -906,6 +906,14 @@ version = "0.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" +[[package]] +name = "wasi-config" +version = "0.1.0" +dependencies = [ + "helper", + "wit-bindgen 0.16.0", +] + [[package]] name = "wasi-http-rc-2023-11-10" version = "0.1.0" diff --git a/tests/test-components/components/wasi-config/Cargo.toml b/tests/test-components/components/wasi-config/Cargo.toml new file mode 100644 index 0000000000..558e708682 --- /dev/null +++ b/tests/test-components/components/wasi-config/Cargo.toml @@ -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" diff --git a/tests/test-components/components/wasi-config/README.md b/tests/test-components/components/wasi-config/README.md new file mode 100644 index 0000000000..7492706f45 --- /dev/null +++ b/tests/test-components/components/wasi-config/README.md @@ -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" diff --git a/tests/test-components/components/wasi-config/src/lib.rs b/tests/test-components/components/wasi-config/src/lib.rs new file mode 100644 index 0000000000..6e9b5706a7 --- /dev/null +++ b/tests/test-components/components/wasi-config/src/lib.rs @@ -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(()) + } +} diff --git a/wit/deps/spin@3.0.0/world.wit b/wit/deps/spin@3.0.0/world.wit index e920055a9a..b38f1f4630 100644 --- a/wit/deps/spin@3.0.0/world.wit +++ b/wit/deps/spin@3.0.0/world.wit @@ -9,4 +9,5 @@ world http-trigger { /// The imports needed for a guest to run on a Spin host world platform { include fermyon:spin/platform@2.0.0; + import wasi:config/store@0.2.0-draft-2024-09-27; } diff --git a/wit/deps/wasi-runtime-config-2024-09-27/store.wit b/wit/deps/wasi-runtime-config-2024-09-27/store.wit new file mode 100644 index 0000000000..794379a754 --- /dev/null +++ b/wit/deps/wasi-runtime-config-2024-09-27/store.wit @@ -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`. 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, 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>, error>; +} diff --git a/wit/deps/wasi-runtime-config-2024-09-27/world.wit b/wit/deps/wasi-runtime-config-2024-09-27/world.wit new file mode 100644 index 0000000000..e879af51b1 --- /dev/null +++ b/wit/deps/wasi-runtime-config-2024-09-27/world.wit @@ -0,0 +1,6 @@ +package wasi:config@0.2.0-draft-2024-09-27; + +world imports { + /// The interface for wasi:config/store + import store; +} \ No newline at end of file