Skip to content

Spin Factors #2519

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 22 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
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
316 changes: 205 additions & 111 deletions Cargo.lock

Large diffs are not rendered by default.

8 changes: 8 additions & 0 deletions crates/app/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -296,6 +296,14 @@ impl<'a, L> AppComponent<'a, L> {
&self.locked.source
}

/// Returns an iterator of environment variable (key, value) pairs.
pub fn environment(&self) -> impl IntoIterator<Item = (&str, &str)> {
self.locked
.env
.iter()
.map(|(k, v)| (k.as_str(), v.as_str()))
}

/// Returns an iterator of [`ContentPath`]s for this component's configured
/// "directory mounts".
pub fn files(&self) -> std::slice::Iter<ContentPath> {
Expand Down
10 changes: 10 additions & 0 deletions crates/expressions/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ use std::{borrow::Cow, collections::HashMap, fmt::Debug};

use spin_locked_app::Variable;

pub use async_trait;

pub use provider::Provider;
use template::Part;
pub use template::Template;
Expand Down Expand Up @@ -251,6 +253,14 @@ impl<'a> Key<'a> {
}
}

impl<'a> TryFrom<&'a str> for Key<'a> {
type Error = Error;

fn try_from(value: &'a str) -> std::prelude::v1::Result<Self, Self::Error> {
Self::new(value)
}
}

impl<'a> AsRef<str> for Key<'a> {
fn as_ref(&self) -> &str {
self.0
Expand Down
17 changes: 17 additions & 0 deletions crates/factor-key-value/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
[package]
name = "spin-factor-key-value"
version = { workspace = true }
authors = { workspace = true }
edition = { workspace = true }

[dependencies]
anyhow = "1.0"
serde = { version = "1.0", features = ["rc"] }
spin-factors = { path = "../factors" }
# TODO: merge with this crate
spin-key-value = { path = "../key-value" }
spin-world = { path = "../world" }
toml = "0.8"

[lints]
workspace = true
164 changes: 164 additions & 0 deletions crates/factor-key-value/src/lib.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,164 @@
mod store;

use std::{
collections::{HashMap, HashSet},
sync::Arc,
};

use anyhow::{bail, ensure};
use serde::Deserialize;
use spin_factors::{
anyhow::{self, Context},
ConfigureAppContext, Factor, FactorInstanceBuilder, FactorRuntimeConfig, InitContext,
InstanceBuilders, PrepareContext, RuntimeFactors,
};
use spin_key_value::{
CachingStoreManager, DelegatingStoreManager, KeyValueDispatch, StoreManager,
KEY_VALUE_STORES_KEY,
};
use store::{store_from_toml_fn, StoreFromToml};

pub use store::MakeKeyValueStore;

#[derive(Default)]
pub struct KeyValueFactor {
store_types: HashMap<&'static str, StoreFromToml>,
}

impl KeyValueFactor {
pub fn add_store_type<T: MakeKeyValueStore>(&mut self, store_type: T) -> anyhow::Result<()> {
if self
.store_types
.insert(T::RUNTIME_CONFIG_TYPE, store_from_toml_fn(store_type))
.is_some()
{
bail!(
"duplicate key value store type {:?}",
T::RUNTIME_CONFIG_TYPE
);
}
Ok(())
}
}

impl Factor for KeyValueFactor {
type RuntimeConfig = RuntimeConfig;
type AppState = AppState;
type InstanceBuilder = InstanceBuilder;

fn init<Factors: RuntimeFactors>(
&mut self,
mut ctx: InitContext<Factors, Self>,
) -> anyhow::Result<()> {
ctx.link_bindings(spin_world::v1::key_value::add_to_linker)?;
ctx.link_bindings(spin_world::v2::key_value::add_to_linker)?;
Ok(())
}

fn configure_app<T: RuntimeFactors>(
&self,
mut ctx: ConfigureAppContext<T, Self>,
) -> anyhow::Result<Self::AppState> {
// Build StoreManager from runtime config
let mut stores = HashMap::new();
if let Some(runtime_config) = ctx.take_runtime_config() {
for (label, StoreConfig { type_, config }) in runtime_config.store_configs {
let store_maker = self
.store_types
.get(type_.as_str())
.with_context(|| format!("unknown key value store type {type_:?}"))?;
let store = store_maker(config)?;
stores.insert(label, store);
}
}
let delegating_manager = DelegatingStoreManager::new(stores);
let caching_manager = CachingStoreManager::new(delegating_manager);
let store_manager = Arc::new(caching_manager);

// Build component -> allowed stores map
let mut component_allowed_stores = HashMap::new();
for component in ctx.app().components() {
let component_id = component.id().to_string();
let key_value_stores = component
.get_metadata(KEY_VALUE_STORES_KEY)?
.unwrap_or_default()
.into_iter()
.collect::<HashSet<_>>();
for label in &key_value_stores {
// TODO: port nicer errors from KeyValueComponent (via error type?)
ensure!(
store_manager.is_defined(label),
"unknown key_value_stores label {label:?} for component {component_id:?}"
);
}
component_allowed_stores.insert(component_id, key_value_stores);
// TODO: warn (?) on unused store?
}

Ok(AppState {
store_manager,
component_allowed_stores,
})
}

fn prepare<T: RuntimeFactors>(
&self,
ctx: PrepareContext<Self>,
_builders: &mut InstanceBuilders<T>,
) -> anyhow::Result<InstanceBuilder> {
let app_state = ctx.app_state();
let allowed_stores = app_state
.component_allowed_stores
.get(ctx.app_component().id())
.expect("component should be in component_stores")
.clone();
Ok(InstanceBuilder {
store_manager: app_state.store_manager.clone(),
allowed_stores,
})
}
}

#[derive(Deserialize)]
#[serde(transparent)]
pub struct RuntimeConfig {
store_configs: HashMap<String, StoreConfig>,
}

impl FactorRuntimeConfig for RuntimeConfig {
const KEY: &'static str = "key_value_store";
}

#[derive(Deserialize)]
struct StoreConfig {
#[serde(rename = "type")]
type_: String,
#[serde(flatten)]
config: toml::Table,
}

type AppStoreManager = CachingStoreManager<DelegatingStoreManager>;

pub struct AppState {
store_manager: Arc<AppStoreManager>,
component_allowed_stores: HashMap<String, HashSet<String>>,
}

pub struct InstanceBuilder {
store_manager: Arc<AppStoreManager>,
allowed_stores: HashSet<String>,
}

impl FactorInstanceBuilder for InstanceBuilder {
type InstanceState = KeyValueDispatch;

fn build(self) -> anyhow::Result<Self::InstanceState> {
let Self {
store_manager,
allowed_stores,
} = self;
let mut dispatch = KeyValueDispatch::new_with_capacity(u32::MAX);
dispatch.init(allowed_stores, store_manager);
Ok(dispatch)
}
}
24 changes: 24 additions & 0 deletions crates/factor-key-value/src/store.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
use std::sync::Arc;

use serde::de::DeserializeOwned;
use spin_key_value::StoreManager;

pub trait MakeKeyValueStore: 'static {
const RUNTIME_CONFIG_TYPE: &'static str;

type RuntimeConfig: DeserializeOwned;
type StoreManager: StoreManager;

fn make_store(&self, runtime_config: Self::RuntimeConfig)
-> anyhow::Result<Self::StoreManager>;
}

pub(crate) type StoreFromToml = Box<dyn Fn(toml::Table) -> anyhow::Result<Arc<dyn StoreManager>>>;

pub(crate) fn store_from_toml_fn<T: MakeKeyValueStore>(provider_type: T) -> StoreFromToml {
Box::new(move |table| {
let runtime_config: T::RuntimeConfig = table.try_into()?;
let provider = provider_type.make_store(runtime_config)?;
Ok(Arc::new(provider))
})
}
18 changes: 18 additions & 0 deletions crates/factor-outbound-http/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
[package]
name = "spin-factor-outbound-http"
version = { workspace = true }
authors = { workspace = true }
edition = { workspace = true }

[dependencies]
anyhow = "1.0"
http = "1.1.0"
spin-factor-outbound-networking = { path = "../factor-outbound-networking" }
spin-factor-wasi = { path = "../factor-wasi" }
spin-factors = { path = "../factors" }
spin-world = { path = "../world" }
tracing = { workspace = true }
wasmtime-wasi-http = { workspace = true }

[lints]
workspace = true
56 changes: 56 additions & 0 deletions crates/factor-outbound-http/src/lib.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
mod spin;
mod wasi;

use spin_factor_outbound_networking::{OutboundAllowedHosts, OutboundNetworkingFactor};
use spin_factors::{
anyhow, ConfigureAppContext, Factor, InstanceBuilders, PrepareContext, RuntimeFactors,
SelfInstanceBuilder,
};
use wasmtime_wasi_http::WasiHttpCtx;

pub use wasi::get_wasi_http_view;

pub struct OutboundHttpFactor;

impl Factor for OutboundHttpFactor {
type RuntimeConfig = ();
type AppState = ();
type InstanceBuilder = InstanceState;

fn init<T: RuntimeFactors>(
&mut self,
mut ctx: spin_factors::InitContext<T, Self>,
) -> anyhow::Result<()> {
ctx.link_bindings(spin_world::v1::http::add_to_linker)?;
wasi::add_to_linker::<T>(ctx.linker())?;
Ok(())
}

fn configure_app<T: RuntimeFactors>(
&self,
_ctx: ConfigureAppContext<T, Self>,
) -> anyhow::Result<Self::AppState> {
Ok(())
}

fn prepare<T: RuntimeFactors>(
&self,
_ctx: PrepareContext<Self>,
builders: &mut InstanceBuilders<T>,
) -> anyhow::Result<Self::InstanceBuilder> {
let allowed_hosts = builders
.get_mut::<OutboundNetworkingFactor>()?
.allowed_hosts();
Ok(InstanceState {
allowed_hosts,
wasi_http_ctx: WasiHttpCtx::new(),
})
}
}

pub struct InstanceState {
allowed_hosts: OutboundAllowedHosts,
wasi_http_ctx: WasiHttpCtx,
}

impl SelfInstanceBuilder for InstanceState {}
31 changes: 31 additions & 0 deletions crates/factor-outbound-http/src/spin.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
use spin_factor_outbound_networking::OutboundUrl;
use spin_world::{
async_trait,
v1::http,
v1::http_types::{self, HttpError, Request, Response},
};

#[async_trait]
impl http::Host for crate::InstanceState {
async fn send_request(&mut self, req: Request) -> Result<Response, HttpError> {
// FIXME(lann): This is all just a stub to test allowed_outbound_hosts
let outbound_url = OutboundUrl::parse(&req.uri, "https").or(Err(HttpError::InvalidUrl))?;
match self.allowed_hosts.allows(&outbound_url).await {
Ok(true) => (),
_ => {
return Err(HttpError::DestinationNotAllowed);
}
}
Ok(Response {
status: 200,
headers: None,
body: Some(b"test response".into()),
})
}
}

impl http_types::Host for crate::InstanceState {
fn convert_http_error(&mut self, err: HttpError) -> anyhow::Result<HttpError> {
Ok(err)
}
}
Loading
Loading