diff --git a/Cargo.lock b/Cargo.lock index e3ebe3a..5413e3c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -829,6 +829,14 @@ dependencies = [ "version_check", ] +[[package]] +name = "get-config" +version = "0.1.0" +dependencies = [ + "anyhow", + "wit-bindgen", +] + [[package]] name = "get-env" version = "0.1.0" diff --git a/Cargo.toml b/Cargo.toml index 664c5b4..7fde63d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -15,6 +15,7 @@ members = [ "virtual-adapter", "tests/components/do-everything", "tests/components/file-read", + "tests/components/get-config", "tests/components/get-env", "tests/components/stdio", ] diff --git a/README.md b/README.md index 49d0a3c..22e1424 100644 --- a/README.md +++ b/README.md @@ -18,6 +18,7 @@ Supports all of the current WASI subsystems: - [Clocks](#clocks): Allow / Deny - [Environment](#env): Set environment variables, configure host environment variable permissions +- [Runtime Config](#runtime-config): Set runtime configuration, configure host property permissions - [Exit](#exit): Allow / Deny - [Filesystem](#filesystem): Mount a read-only filesystem, configure host filesystem preopen remappings or pass-through. - [HTTP](#http): Allow / Deny @@ -82,6 +83,19 @@ wasi-virt component.wasm -e CUSTOM=VAR --allow-env -o virt.wasm wasi-virt component.wasm -e CUSTOM=VAR --allow-env=SOME,ENV_VARS -o virt.wasm ``` +### Runtime Config + +```sh +# Setting specific config properties (while disallowing all host config property access): +wasi-virt component.wasm -c custom=prop -o virt.wasm + +# Setting config properties with all host config properties allowed: +wasi-virt component.wasm -c custom=prop --allow-config -o virt.wasm + +# Setting config properties with restricted host config property access: +wasi-virt component.wasm -c custom=prop --allow-config=some,property -o virt.wasm +``` + ### Exit ```sh diff --git a/lib/package.wasm b/lib/package.wasm index 1f57d57..d31b54a 100644 Binary files a/lib/package.wasm and b/lib/package.wasm differ diff --git a/lib/virtual_adapter.debug.wasm b/lib/virtual_adapter.debug.wasm index 85b8a13..6cf25b9 100755 Binary files a/lib/virtual_adapter.debug.wasm and b/lib/virtual_adapter.debug.wasm differ diff --git a/lib/virtual_adapter.wasm b/lib/virtual_adapter.wasm index cd92b49..c5378a7 100755 Binary files a/lib/virtual_adapter.wasm and b/lib/virtual_adapter.wasm differ diff --git a/src/bin/wasi-virt.rs b/src/bin/wasi-virt.rs index e078966..f68c030 100644 --- a/src/bin/wasi-virt.rs +++ b/src/bin/wasi-virt.rs @@ -58,6 +58,15 @@ struct Args { #[arg(short, long, use_value_delimiter(true), value_name("ENV=VAR"), value_parser = parse_key_val::, help_heading = "Env")] env: Option>, + // RUNTIME CONFIG + /// Allow unrestricted access to host runtime configuration properties, or to a comma-separated list of property names. + #[arg(long, num_args(0..), use_value_delimiter(true), require_equals(true), value_name("PROPERTY_NAME"), help_heading = "Runtime Config")] + allow_config: Option>, + + /// Set runtime config property overrides + #[arg(short, long, use_value_delimiter(true), value_name("NAME=VALUE"), value_parser = parse_key_val::, help_heading = "Runtime Config")] + config: Option>, + // FS /// Allow unrestricted access to host preopens #[arg(long, default_missing_value="true", num_args=0..=1, help_heading = "Fs")] @@ -165,6 +174,25 @@ fn main() -> Result<()> { env.overrides = env_overrides; } + // config options + let config = virt_opts.config(); + match args.allow_config { + Some(allow_config) if allow_config.len() == 0 => { + config.allow_all(); + } + Some(allow_config) => { + config.allow(&allow_config); + } + None => { + if allow_all { + config.allow_all(); + } + } + }; + if let Some(config_overrides) = args.config { + config.overrides = config_overrides; + } + // fs options let fs = virt_opts.fs(); if let Some(preopens) = args.preopen { diff --git a/src/lib.rs b/src/lib.rs index 3b4df17..f7c89d4 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,6 +1,7 @@ use anyhow::{bail, Context, Result}; use serde::Deserialize; use std::{env, fs, path::PathBuf, time::SystemTime}; +use virt_config::{create_config_virt, strip_config_virt}; use virt_deny::{ deny_clocks_virt, deny_exit_virt, deny_http_virt, deny_random_virt, deny_sockets_virt, }; @@ -14,12 +15,14 @@ use wit_parser::WorldItem; mod data; mod stub_preview1; +mod virt_config; mod virt_deny; mod virt_env; mod virt_io; mod walrus_ops; pub use stub_preview1::stub_preview1; +pub use virt_config::{HostConfig, VirtConfig}; pub use virt_env::{HostEnv, VirtEnv}; pub use virt_io::{FsEntry, StdioCfg, VirtFs, VirtualFiles}; @@ -44,6 +47,8 @@ pub struct WasiVirt { pub debug: bool, /// Environment virtualization pub env: Option, + /// Configuration virtualization + pub config: Option, /// Filesystem virtualization pub fs: Option, /// Stdio virtualization @@ -81,6 +86,7 @@ impl WasiVirt { self.exit(true); self.random(true); self.env().allow_all(); + self.config().allow_all(); self.fs().allow_host_preopens(); self.stdio().allow(); } @@ -92,6 +98,7 @@ impl WasiVirt { self.exit(false); self.random(false); self.env().deny_all(); + self.config().deny_all(); self.fs().deny_host_preopens(); self.stdio().ignore(); } @@ -120,6 +127,10 @@ impl WasiVirt { self.env.get_or_insert_with(Default::default) } + pub fn config(&mut self) -> &mut VirtConfig { + self.config.get_or_insert_with(Default::default) + } + pub fn fs(&mut self) -> &mut VirtFs { self.fs.get_or_insert_with(Default::default) } @@ -165,6 +176,9 @@ impl WasiVirt { if !matches("wasi:cli/environment") { self.env = None; } + if !matches("wasi:config/runtime") { + self.config = None; + } if !matches("wasi:filesystem/") { self.fs = None; } @@ -207,6 +221,9 @@ impl WasiVirt { if let Some(env) = &self.env { create_env_virt(&mut module, env)?; } + if let Some(config) = &self.config { + create_config_virt(&mut module, config)?; + } let has_io = self.fs.is_some() || self.stdio.is_some() @@ -256,6 +273,7 @@ impl WasiVirt { let base_world = resolve.select_world(&pkg_ids, Some("virtual-base"))?; let env_world = resolve.select_world(&pkg_ids, Some("virtual-env"))?; + let config_world = resolve.select_world(&pkg_ids, Some("virtual-config"))?; let io_world = resolve.select_world(&pkg_ids, Some("virtual-io"))?; let io_clocks_world = resolve.select_world(&pkg_ids, Some("virtual-io-clocks"))?; @@ -270,12 +288,17 @@ impl WasiVirt { let http_world = resolve.select_world(&pkg_ids, Some("virtual-http"))?; let sockets_world = resolve.select_world(&pkg_ids, Some("virtual-sockets"))?; - // env, exit & random subsystems are fully independent + // env, config, exit & random subsystems are fully independent if self.env.is_some() { resolve.merge_worlds(env_world, base_world)?; } else { strip_env_virt(&mut module)?; } + if self.config.is_some() { + resolve.merge_worlds(config_world, base_world)?; + } else { + strip_config_virt(&mut module)?; + } if let Some(exit) = self.exit { if !exit { resolve.merge_worlds(exit_world, base_world)?; diff --git a/src/virt_config.rs b/src/virt_config.rs new file mode 100644 index 0000000..ca124a2 --- /dev/null +++ b/src/virt_config.rs @@ -0,0 +1,250 @@ +use anyhow::{bail, Context, Result}; +use serde::Deserialize; +use walrus::{ir::Value, ConstExpr, DataKind, ExportItem, GlobalKind, Module}; + +use crate::walrus_ops::{bump_stack_global, get_active_data_segment}; + +#[derive(Deserialize, Debug, Clone, Default)] +#[serde(deny_unknown_fields)] +pub struct VirtConfig { + /// Set specific configuration property overrides + #[serde(default)] + pub overrides: Vec<(String, String)>, + /// Define how to embed into the host configuration + /// (Pass-through / encapsulate / allow / deny) + #[serde(default)] + pub host: HostConfig, +} + +#[derive(Deserialize, Debug, Clone, Default)] +#[serde(rename_all = "kebab-case", deny_unknown_fields)] +pub enum HostConfig { + /// Fully encapsulate the configuration, removing all host + /// configuration import checks + #[default] + None, + /// Apart from the overrides, pass through all configuration + /// properties from the host + All, + /// Only allow the provided configuration property keys + Allow(Vec), + /// Allow all configuration properties, except the provided keys + Deny(Vec), +} + +impl VirtConfig { + /// Set the host configuration property allow list + pub fn allow(&mut self, allow_list: &[String]) -> &mut Self { + self.host = HostConfig::Allow(allow_list.iter().map(|s| s.to_string()).collect()); + self + } + + /// Set the host configuration property deny list + pub fn deny(&mut self, deny_list: &[&str]) -> &mut Self { + self.host = HostConfig::Deny(deny_list.iter().map(|s| s.to_string()).collect()); + self + } + + /// Enable all configuration properties on the host + pub fn allow_all(&mut self) -> &mut Self { + self.host = HostConfig::All; + self + } + + /// Deny all configuration properties on the host + pub fn deny_all(&mut self) -> &mut Self { + self.host = HostConfig::None; + self + } + + /// Set the configuration property overrides + pub fn overrides(&mut self, overrides: &[(&str, &str)]) -> &mut Self { + for (key, val) in overrides { + self.overrides.push((key.to_string(), val.to_string())); + } + self + } +} + +pub(crate) fn create_config_virt<'a>(module: &'a mut Module, config: &VirtConfig) -> Result<()> { + let config_ptr_addr = { + let config_ptr_export = module + .exports + .iter() + .find(|expt| expt.name.as_str() == "config") + .context("Adapter 'config' is not exported")?; + let ExportItem::Global(config_ptr_global) = config_ptr_export.item else { + bail!("Adapter 'config' not a global"); + }; + let GlobalKind::Local(ConstExpr::Value(Value::I32(config_ptr_addr))) = + &module.globals.get(config_ptr_global).kind + else { + bail!("Adapter 'config' not a local I32 global value"); + }; + *config_ptr_addr as u32 + }; + + // If host config is disabled, remove its import entirely + // replacing it with a stub panic + if matches!(config.host, HostConfig::None) { + stub_config_virt(module)?; + // we do arguments as well because virt assumes reactors for now... + } + + let memory = module.get_memory_id()?; + + // prepare the field data list vector for writing + // strings must be sorted as binary searches are used against this data + let mut field_data_vec: Vec<&str> = Vec::new(); + let mut sorted_overrides = config.overrides.clone(); + sorted_overrides.sort_by(|(a, _), (b, _)| a.cmp(b)); + for (key, value) in &sorted_overrides { + field_data_vec.push(key.as_ref()); + field_data_vec.push(value.as_ref()); + } + match &config.host { + HostConfig::Allow(allow_list) => { + let mut allow_list: Vec<&str> = allow_list.iter().map(|item| item.as_ref()).collect(); + allow_list.sort(); + for key in allow_list { + field_data_vec.push(key); + } + } + HostConfig::Deny(deny_list) => { + let mut deny_list: Vec<&str> = deny_list.iter().map(|item| item.as_ref()).collect(); + deny_list.sort(); + for key in deny_list { + field_data_vec.push(key); + } + } + _ => {} + } + + let mut field_data_bytes = Vec::new(); + for str in field_data_vec { + assert!(field_data_bytes.len() % 4 == 0); + // write the length at the aligned offset + field_data_bytes.extend_from_slice(&(str.len() as u32).to_le_bytes()); + let str_bytes = str.as_bytes(); + field_data_bytes.extend_from_slice(str_bytes); + let rem = str_bytes.len() % 4; + // add padding for alignment if necessary + if rem > 0 { + field_data_bytes.extend((0..4 - rem).map(|_| 0)); + } + } + + if field_data_bytes.len() % 8 != 0 { + field_data_bytes.resize(field_data_bytes.len() + 4, 0); + } + + let field_data_addr = if field_data_bytes.len() > 0 { + // Offset the stack global by the static field data length + let field_data_addr = bump_stack_global(module, field_data_bytes.len() as i32)?; + + // Add a new data segment for this new range created at the top of the stack + module.data.add( + DataKind::Active { + memory, + offset: ConstExpr::Value(Value::I32(field_data_addr as i32)), + }, + field_data_bytes, + ); + Some(field_data_addr) + } else { + None + }; + + // In the existing static data segment, update the static data options. + // + // From virtual-adapter/src/config.rs: + // + // #[repr(C)] + // pub struct Config { + // /// Whether to fallback to the host config + // /// [byte 0] + // host_fallback: bool, + // /// Whether we are providing an allow list or a deny list + // /// on the fallback lookups + // /// [byte 1] + // host_fallback_allow: bool, + // /// How many host fields are defined in the data pointer + // /// [byte 4] + // host_field_cnt: u32, + // /// Host many host fields are defined to be allow or deny + // /// (these are concatenated at the end of the data with empty values) + // /// [byte 8] + // host_allow_or_deny_cnt: u32, + // /// Byte data of u32 byte len followed by string bytes + // /// up to the lengths previously provided. + // /// [byte 12] + // host_field_data: *const u8, + // } + let (data, data_offset) = get_active_data_segment(module, memory, config_ptr_addr)?; + let bytes = data.value.as_mut_slice(); + + let host_field_cnt = config.overrides.len() as u32; + bytes[data_offset + 4..data_offset + 8].copy_from_slice(&host_field_cnt.to_le_bytes()); + match &config.host { + // All is already the default data + HostConfig::All => {} + HostConfig::None => { + bytes[data_offset] = 0; + } + HostConfig::Allow(allow_list) => { + bytes[data_offset + 1] = 1; + bytes[data_offset + 8..data_offset + 12] + .copy_from_slice(&(allow_list.len() as u32).to_le_bytes()); + } + HostConfig::Deny(deny_list) => { + bytes[data_offset + 1] = 0; + bytes[data_offset + 8..data_offset + 12] + .copy_from_slice(&(deny_list.len() as u32).to_le_bytes()); + } + }; + if let Some(field_data_addr) = field_data_addr { + bytes[data_offset + 12..data_offset + 16].copy_from_slice(&field_data_addr.to_le_bytes()); + } + + Ok(()) +} + +/// Functions that represent the configuration functionality provided by WASI CLI +const WASI_CONFIG_FNS: [&str; 2] = ["get", "get-all"]; + +/// Stub imported functions that implement the WASI runtime config functionality +/// +/// This function throws an error if any imported functions do not exist +pub(crate) fn stub_config_virt(module: &mut Module) -> Result<()> { + for fn_name in WASI_CONFIG_FNS { + module.replace_imported_func( + module + .imports + .get_func("wasi:config/runtime@0.2.0-draft", fn_name)?, + |(body, _)| { + body.unreachable(); + }, + )?; + } + + Ok(()) +} + +/// Strip exported functions that implement the WASI runtime config functionality +pub(crate) fn strip_config_virt(module: &mut Module) -> Result<()> { + stub_config_virt(module)?; + + for fn_name in WASI_CONFIG_FNS { + let Ok(fid) = module + .exports + .get_func(format!("wasi:config/runtime@0.2.0-draft#{fn_name}")) + else { + bail!("Expected CLI function {fn_name}") + }; + module.replace_exported_func(fid, |(body, _)| { + body.unreachable(); + })?; + } + + Ok(()) +} diff --git a/tests/cases/config-allow.toml b/tests/cases/config-allow.toml new file mode 100644 index 0000000..2c65057 --- /dev/null +++ b/tests/cases/config-allow.toml @@ -0,0 +1,14 @@ +component = "get-config" + +[host-config] +private_token = "private" +public_prop = "val" + +[virt-opts.config] +overrides = [["custom", "val"]] + +[virt-opts.config.host] +allow = ["public_prop"] + +[expect] +config = [["custom", "val"], ["public_prop", "val"]] diff --git a/tests/cases/config-deny.toml b/tests/cases/config-deny.toml new file mode 100644 index 0000000..7fe72c9 --- /dev/null +++ b/tests/cases/config-deny.toml @@ -0,0 +1,14 @@ +component = "get-config" + +[host-config] +private_token = "private" +public_prop = "val" + +[virt-opts.config] +overrides = [["custom", "val"]] + +[virt-opts.config.host] +deny = ["private_token"] + +[expect] +config = [["custom", "val"], ["public_prop", "val"]] diff --git a/tests/cases/config-none-overrides.toml b/tests/cases/config-none-overrides.toml new file mode 100644 index 0000000..30c996a --- /dev/null +++ b/tests/cases/config-none-overrides.toml @@ -0,0 +1,11 @@ +component = "get-config" + +[host-config] +custom = "test" + +[virt-opts.config] +host = "none" +overrides = [["prop_override", "Value"]] + +[expect] +config = [["prop_override", "Value"]] diff --git a/tests/cases/config-none.toml b/tests/cases/config-none.toml new file mode 100644 index 0000000..4af9621 --- /dev/null +++ b/tests/cases/config-none.toml @@ -0,0 +1,11 @@ +component = "get-config" + +[host-config] +custom = "test" + +[virt-opts.config] +host = "none" +overrides = [] + +[expect] +config = [] diff --git a/tests/cases/config-passthrough.toml b/tests/cases/config-passthrough.toml new file mode 100644 index 0000000..5171fcb --- /dev/null +++ b/tests/cases/config-passthrough.toml @@ -0,0 +1,11 @@ +component = "get-config" + +[host-config] +custom = "prop" + +[virt-opts.config] +host = "all" +overrides = [] + +[expect] +config = [["custom", "prop"]] diff --git a/tests/components/do-everything/src/lib.rs b/tests/components/do-everything/src/lib.rs index c613abb..bda623e 100644 --- a/tests/components/do-everything/src/lib.rs +++ b/tests/components/do-everything/src/lib.rs @@ -17,6 +17,9 @@ impl Guest for VirtTestComponent { fn test_get_env() -> Vec<(String, String)> { unreachable!(); } + fn test_get_config() -> Vec<(String, String)> { + unreachable!(); + } fn test_file_read(path: String) -> String { let vars: Vec<(String, String)> = env::vars().collect(); let mut rng = rand::thread_rng(); diff --git a/tests/components/file-read/src/lib.rs b/tests/components/file-read/src/lib.rs index ce74c84..02fc4d5 100644 --- a/tests/components/file-read/src/lib.rs +++ b/tests/components/file-read/src/lib.rs @@ -12,6 +12,9 @@ impl Guest for VirtTestComponent { fn test_get_env() -> Vec<(String, String)> { Vec::new() } + fn test_get_config() -> Vec<(String, String)> { + Vec::new() + } fn test_file_read(path: String) -> String { let meta = match fs::metadata(&path) { Ok(meta) => meta, diff --git a/tests/components/get-config/Cargo.toml b/tests/components/get-config/Cargo.toml new file mode 100644 index 0000000..1143778 --- /dev/null +++ b/tests/components/get-config/Cargo.toml @@ -0,0 +1,11 @@ +[package] +name = "get-config" +version = "0.1.0" +publish = false + +[lib] +crate-type = ["cdylib"] + +[dependencies] +anyhow = { workspace = true } +wit-bindgen = { workspace = true } diff --git a/tests/components/get-config/src/lib.rs b/tests/components/get-config/src/lib.rs new file mode 100644 index 0000000..074979d --- /dev/null +++ b/tests/components/get-config/src/lib.rs @@ -0,0 +1,26 @@ +use std::env; + +wit_bindgen::generate!({ + path: "../../../wit", + world: "virt-test", + generate_all +}); + +struct VirtTestComponent; + +impl Guest for VirtTestComponent { + fn test_get_env() -> Vec<(String, String)> { + unimplemented!(); + } + fn test_get_config() -> Vec<(String, String)> { + wasi::config::runtime::get_all().unwrap() + } + fn test_file_read(_path: String) -> String { + unimplemented!(); + } + fn test_stdio() -> () { + unimplemented!(); + } +} + +export!(VirtTestComponent); diff --git a/tests/components/get-env/src/lib.rs b/tests/components/get-env/src/lib.rs index ea672d8..f17f8b5 100644 --- a/tests/components/get-env/src/lib.rs +++ b/tests/components/get-env/src/lib.rs @@ -12,6 +12,9 @@ impl Guest for VirtTestComponent { fn test_get_env() -> Vec<(String, String)> { env::vars().collect() } + fn test_get_config() -> Vec<(String, String)> { + unimplemented!(); + } fn test_file_read(_path: String) -> String { unimplemented!(); } diff --git a/tests/components/stdio/src/lib.rs b/tests/components/stdio/src/lib.rs index 441c3f1..05057ef 100644 --- a/tests/components/stdio/src/lib.rs +++ b/tests/components/stdio/src/lib.rs @@ -12,6 +12,9 @@ impl Guest for VirtTestComponent { fn test_get_env() -> Vec<(String, String)> { unimplemented!(); } + fn test_get_config() -> Vec<(String, String)> { + unimplemented!(); + } fn test_file_read(_path: String) -> String { unimplemented!(); } diff --git a/tests/virt.rs b/tests/virt.rs index 7444b77..a28cda0 100644 --- a/tests/virt.rs +++ b/tests/virt.rs @@ -2,6 +2,7 @@ use anyhow::{anyhow, bail, Context, Result}; use heck::ToSnakeCase; use serde::Deserialize; use std::collections::BTreeMap; +use std::pin::Pin; use std::process::Command; use std::{fs, path::PathBuf}; use wasi_virt::WasiVirt; @@ -47,6 +48,7 @@ fn cmd(arg: &str) -> Result<()> { #[serde(rename_all = "kebab-case", deny_unknown_fields)] struct TestExpectation { env: Option>, + config: Option>, file_read: Option, encapsulation: Option, stdout: Option, @@ -66,6 +68,7 @@ struct TestCase { component: String, compose: Option, host_env: Option>, + host_config: Option>, host_fs_path: Option, virt_opts: Option, expect: TestExpectation, @@ -217,6 +220,15 @@ async fn virt_test() -> Result<()> { builder.env(k, v); } } + let props: Vec<(String, String)> = { + let mut props = vec![]; + if let Some(host_config) = &test.host_config { + for (k, v) in host_config { + props.push((k.clone(), v.clone())) + } + } + props + }; let table = ResourceTable::new(); let wasi = builder.build(); @@ -234,6 +246,7 @@ async fn virt_test() -> Result<()> { struct CommandCtx { table: ResourceTable, wasi: WasiCtx, + props: Vec<(String, String)>, } impl WasiView for CommandCtx { fn table(&mut self) -> &mut ResourceTable { @@ -243,9 +256,19 @@ async fn virt_test() -> Result<()> { &mut self.wasi } } + impl CommandCtx { + fn config(&self) -> Vec<(String, String)> { + self.props.clone() + } + } wasmtime_wasi::add_to_linker_async(&mut linker)?; - let mut store = Store::new(&engine, CommandCtx { table, wasi }); + wasi::config::runtime::add_to_linker_get_host(&mut linker, |ctx: &mut CommandCtx| { + StubConfig { + props: ctx.config(), + } + })?; + let mut store = Store::new(&engine, CommandCtx { table, wasi, props }); let (instance, _instance) = VirtTest::instantiate_async(&mut store, &component, &linker).await?; @@ -273,6 +296,25 @@ async fn virt_test() -> Result<()> { } } + // config property expectation check + if let Some(expect_config) = &test.expect.config { + let config_props = instance.call_test_get_config(&mut store).await?; + if !config_props.eq(expect_config) { + return Err(anyhow!( + "Unexpected config properties testing {:?}: + + \x1b[1mExpected:\x1b[0m {:?} + \x1b[1mActual:\x1b[0m {:?} + + {:?}", + test_case_path, + expect_config, + config_props, + test + )); + } + } + // fs read expectation check if let Some(expect_file_read) = &test.expect.file_read { let file_read = instance @@ -396,3 +438,65 @@ fn collect_component_imports(component_bytes: Vec) -> Result> { Ok(import_ids) } + +// TODO remove this stub once wasi:runtime/config is implemented by wasmtime +struct StubConfig { + props: Vec<(String, String)>, +} +impl wasi::config::runtime::Host for StubConfig { + #[doc = " Gets a single opaque config value set at the given key if it exists"] + #[must_use] + #[allow(clippy::type_complexity, clippy::type_repetition_in_bounds)] + fn get<'life0, 'async_trait>( + &'life0 mut self, + key: String, + ) -> ::core::pin::Pin< + Box< + dyn ::core::future::Future< + Output = Result, wasi::config::runtime::ConfigError>, + > + ::core::marker::Send + + 'async_trait, + >, + > + where + 'life0: 'async_trait, + Self: 'async_trait, + { + let mut result = Ok(None); + for (k, v) in self.props.iter() { + if *k == key { + result = Ok(Some(v.clone())); + } + } + let result = std::future::ready(result); + let result = Box::new(result); + let result = Pin::new(result); + + result + } + + #[doc = " Gets a list of all set config data"] + #[must_use] + #[allow(clippy::type_complexity, clippy::type_repetition_in_bounds)] + fn get_all<'life0, 'async_trait>( + &'life0 mut self, + ) -> ::core::pin::Pin< + Box< + dyn ::core::future::Future< + Output = Result, wasi::config::runtime::ConfigError>, + > + ::core::marker::Send + + 'async_trait, + >, + > + where + 'life0: 'async_trait, + Self: 'async_trait, + { + let result = Ok(self.props.clone()); + let result = std::future::ready(result); + let result = Box::new(result); + let result = Pin::new(result); + + result + } +} diff --git a/virtual-adapter/src/config.rs b/virtual-adapter/src/config.rs new file mode 100644 index 0000000..a7d6324 --- /dev/null +++ b/virtual-adapter/src/config.rs @@ -0,0 +1,118 @@ +use crate::exports::wasi::config::runtime::{ConfigError, Guest as Runtime}; +use crate::wasi::config::runtime; +use crate::VirtAdapter; + +#[repr(C)] +pub struct Config { + /// Whether to fallback to the host config + /// [byte 0] + host_fallback: bool, + /// Whether we are providing an allow list or a deny list + /// on the fallback lookups + /// [byte 1] + host_fallback_allow: bool, + /// How many host fields are defined in the data pointer + /// [byte 4] + host_field_cnt: u32, + /// Host many host fields are defined to be allow or deny + /// (these are concatenated at the end of the data with empty values) + /// [byte 8] + host_allow_or_deny_cnt: u32, + /// Byte data of u32 byte len followed by string bytes + /// up to the lengths previously provided. + /// [byte 12] + host_field_data: *const u8, +} + +#[no_mangle] +pub static mut config: Config = Config { + host_fallback: true, + host_fallback_allow: false, + host_field_cnt: 0, + host_allow_or_deny_cnt: 0, + host_field_data: 0 as *const u8, +}; + +fn read_data_str(offset: &mut isize) -> &'static str { + let data: *const u8 = unsafe { config.host_field_data.offset(*offset) }; + let byte_len = unsafe { (data as *const u32).read() } as usize; + *offset += 4; + let data: *const u8 = unsafe { config.host_field_data.offset(*offset) }; + let str_data = unsafe { std::slice::from_raw_parts(data, byte_len) }; + *offset += byte_len as isize; + let rem = *offset % 4; + if rem > 0 { + *offset += 4 - rem; + } + unsafe { core::str::from_utf8_unchecked(str_data) } +} + +impl Runtime for VirtAdapter { + fn get(key: String) -> Result, ConfigError> { + let mut data_offset: isize = 0; + for _ in 0..unsafe { config.host_field_cnt } { + let config_key = read_data_str(&mut data_offset); + let config_val = read_data_str(&mut data_offset); + if key == config_key.to_string() { + return Ok(Some(config_val.to_string())); + } + } + + // fallback ASSUMES that all data is alphabetically ordered + if unsafe { config.host_fallback } { + let mut allow_or_deny = Vec::new(); + for _ in 0..unsafe { config.host_allow_or_deny_cnt } { + let allow_or_deny_key = read_data_str(&mut data_offset); + allow_or_deny.push(allow_or_deny_key); + } + + let is_allow_list = unsafe { config.host_fallback_allow }; + let in_list = allow_or_deny.binary_search(&key.as_ref()).is_ok(); + if is_allow_list && in_list || !is_allow_list && !in_list { + return runtime::get(&key).map_err(config_err_map); + } + } + Ok(None) + } + + fn get_all() -> Result, ConfigError> { + let mut configuration = Vec::new(); + let mut data_offset: isize = 0; + for _ in 0..unsafe { config.host_field_cnt } { + let config_key = read_data_str(&mut data_offset); + let config_val = read_data_str(&mut data_offset); + configuration.push((config_key.to_string(), config_val.to_string())); + } + let override_len = configuration.len(); + // fallback ASSUMES that all data is alphabetically ordered + if unsafe { config.host_fallback } { + let mut allow_or_deny = Vec::new(); + for _ in 0..unsafe { config.host_allow_or_deny_cnt } { + let allow_or_deny_key = read_data_str(&mut data_offset); + allow_or_deny.push(allow_or_deny_key); + } + + let is_allow_list = unsafe { config.host_fallback_allow }; + for (key, value) in runtime::get_all().map_err(config_err_map)? { + if configuration[0..override_len] + .binary_search_by_key(&&key, |(s, _)| s) + .is_ok() + { + continue; + } + let in_list = allow_or_deny.binary_search(&key.as_ref()).is_ok(); + if is_allow_list && in_list || !is_allow_list && !in_list { + configuration.push((key, value)); + } + } + } + Ok(configuration) + } +} + +fn config_err_map(err: runtime::ConfigError) -> ConfigError { + match err { + runtime::ConfigError::Upstream(msg) => ConfigError::Upstream(msg), + runtime::ConfigError::Io(msg) => ConfigError::Io(msg), + } +} diff --git a/virtual-adapter/src/lib.rs b/virtual-adapter/src/lib.rs index b328a9f..aab68d3 100644 --- a/virtual-adapter/src/lib.rs +++ b/virtual-adapter/src/lib.rs @@ -1,6 +1,7 @@ #![no_main] #![feature(ptr_sub_ptr)] +mod config; mod env; mod io; diff --git a/wit/deps/config/runtime_config.wit b/wit/deps/config/runtime_config.wit new file mode 100644 index 0000000..29d53b1 --- /dev/null +++ b/wit/deps/config/runtime_config.wit @@ -0,0 +1,25 @@ +interface runtime { + /// An error type that encapsulates the different errors that can occur fetching config + variant config-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 single opaque config value set at the given key if it exists + get: func( + /// A string key to fetch + key: string + ) -> result, config-error>; + + /// Gets a list of all set config data + get-all: func() -> result>, config-error>; +} diff --git a/wit/deps/config/world.wit b/wit/deps/config/world.wit new file mode 100644 index 0000000..378c4a7 --- /dev/null +++ b/wit/deps/config/world.wit @@ -0,0 +1,6 @@ +package wasi:config@0.2.0-draft; + +world imports { + /// The runtime interface for config + import runtime; +} \ No newline at end of file diff --git a/wit/virt.wit b/wit/virt.wit index 7ed1cfb..612da37 100644 --- a/wit/virt.wit +++ b/wit/virt.wit @@ -43,6 +43,8 @@ world virtual-adapter { export wasi:sockets/tcp@0.2.0; import wasi:sockets/udp@0.2.0; export wasi:sockets/udp@0.2.0; + import wasi:config/runtime@0.2.0-draft; + export wasi:config/runtime@0.2.0-draft; } world virtual-base { @@ -160,6 +162,11 @@ world virtual-exit { export wasi:cli/exit@0.2.0; } +world virtual-config { + import wasi:config/runtime@0.2.0-draft; + export wasi:config/runtime@0.2.0-draft; +} + world virt-test { import wasi:clocks/wall-clock@0.2.0; import wasi:clocks/monotonic-clock@0.2.0; @@ -177,6 +184,7 @@ world virt-test { import wasi:io/poll@0.2.0; import wasi:io/streams@0.2.0; import wasi:cli/environment@0.2.0; + import wasi:config/runtime@0.2.0-draft; import wasi:filesystem/preopens@0.2.0; import wasi:cli/exit@0.2.0; import wasi:cli/stdin@0.2.0; @@ -189,6 +197,7 @@ world virt-test { import wasi:cli/terminal-stderr@0.2.0; export test-get-env: func() -> list>; + export test-get-config: func() -> list>; export test-file-read: func(path: string) -> string; export test-stdio: func() -> (); }