Skip to content

Commit

Permalink
Merge pull request #2658 from fermyon/ensure_allowed_databases_are_co…
Browse files Browse the repository at this point in the history
…nfigured
  • Loading branch information
rylev authored Jul 19, 2024
2 parents 69bc40f + 4cb982d commit 0a9f22a
Show file tree
Hide file tree
Showing 2 changed files with 129 additions and 19 deletions.
66 changes: 53 additions & 13 deletions crates/factor-sqlite/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -68,24 +68,31 @@ impl Factor for SqliteFactor {
.map(|component| {
Ok((
component.id().to_string(),
component
.get_metadata(ALLOWED_DATABASES_KEY)?
.unwrap_or_default()
.into_iter()
.collect::<HashSet<_>>()
.into(),
Arc::new(
component
.get_metadata(ALLOWED_DATABASES_KEY)?
.unwrap_or_default()
.into_iter()
.collect::<HashSet<_>>(),
),
))
})
.collect::<anyhow::Result<_>>()?;
.collect::<anyhow::Result<HashMap<_, _>>>()?;
let resolver = self.runtime_config_resolver.clone();
let get_connection_pool: host::ConnectionPoolGetter = Arc::new(move |label| {
connection_pools
.get(label)
.cloned()
.or_else(|| resolver.default(label))
});

ensure_allowed_databases_are_configured(&allowed_databases, |label| {
get_connection_pool(label).is_some()
})?;

Ok(AppState {
allowed_databases,
get_connection_pool: Arc::new(move |label| {
connection_pools
.get(label)
.cloned()
.or_else(|| resolver.default(label))
}),
get_connection_pool,
})
}

Expand All @@ -105,6 +112,39 @@ impl Factor for SqliteFactor {
}
}

/// Ensure that all the databases in the allowed databases list for each component are configured
fn ensure_allowed_databases_are_configured(
allowed_databases: &HashMap<String, Arc<HashSet<String>>>,
is_configured: impl Fn(&str) -> bool,
) -> anyhow::Result<()> {
let mut errors = Vec::new();
for (component_id, allowed_dbs) in allowed_databases {
for allowed in allowed_dbs.iter() {
if !is_configured(allowed) {
errors.push(format!(
"- Component {component_id} uses database '{allowed}'"
));
}
}
}

if !errors.is_empty() {
let prologue = vec![
"One or more components use SQLite databases which are not defined.",
"Check the spelling, or pass a runtime configuration file that defines these stores.",
"See https://developer.fermyon.com/spin/dynamic-configuration#sqlite-storage-runtime-configuration",
"Details:",
];
let lines: Vec<_> = prologue
.into_iter()
.map(|s| s.to_owned())
.chain(errors)
.collect();
return Err(anyhow::anyhow!(lines.join("\n")));
}
Ok(())
}

pub const ALLOWED_DATABASES_KEY: MetadataKey<Vec<String>> = MetadataKey::new("databases");

pub struct AppState {
Expand Down
82 changes: 76 additions & 6 deletions crates/factor-sqlite/tests/factor.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
use std::{collections::HashSet, sync::Arc};

use factor_sqlite::SqliteFactor;
use spin_factors::{anyhow, RuntimeFactors};
use spin_factors::{
anyhow::{self, bail},
RuntimeFactors,
};
use spin_factors_test::{toml, TestEnvironment};

#[derive(RuntimeFactors)]
Expand All @@ -11,7 +14,7 @@ struct TestFactors {

#[tokio::test]
async fn sqlite_works() -> anyhow::Result<()> {
let test_resolver = RuntimeConfigResolver;
let test_resolver = RuntimeConfigResolver::new(Some("default"));
let factors = TestFactors {
sqlite: SqliteFactor::new(test_resolver),
};
Expand All @@ -30,7 +33,60 @@ async fn sqlite_works() -> anyhow::Result<()> {
Ok(())
}

struct RuntimeConfigResolver;
#[tokio::test]
async fn errors_when_non_configured_database_used() -> anyhow::Result<()> {
let test_resolver = RuntimeConfigResolver::new(None);
let factors = TestFactors {
sqlite: SqliteFactor::new(test_resolver),
};
let env = TestEnvironment::default_manifest_extend(toml! {
[component.test-component]
source = "does-not-exist.wasm"
sqlite_databases = ["foo"]
});
let Err(err) = env.build_instance_state(factors).await else {
bail!("Expected build_instance_state to error but it did not");
};

assert!(err
.to_string()
.contains("One or more components use SQLite databases which are not defined."));

Ok(())
}

#[tokio::test]
async fn no_error_when_database_is_configured() -> anyhow::Result<()> {
let test_resolver = RuntimeConfigResolver::new(None);
let factors = TestFactors {
sqlite: SqliteFactor::new(test_resolver),
};
let mut env = TestEnvironment::default_manifest_extend(toml! {
[component.test-component]
source = "does-not-exist.wasm"
sqlite_databases = ["foo"]
});
env.runtime_config = toml! {
[sqlite_database.foo]
type = "sqlite"
};
assert!(env.build_instance_state(factors).await.is_ok());

Ok(())
}

/// Will return an `InvalidConnectionPool` for all runtime configured databases and the supplied default database.
struct RuntimeConfigResolver {
default: Option<String>,
}

impl RuntimeConfigResolver {
fn new(default: Option<&str>) -> Self {
Self {
default: default.map(Into::into),
}
}
}

impl factor_sqlite::runtime_config::RuntimeConfigResolver for RuntimeConfigResolver {
fn get_pool(
Expand All @@ -39,11 +95,25 @@ impl factor_sqlite::runtime_config::RuntimeConfigResolver for RuntimeConfigResol
config: toml::Table,
) -> anyhow::Result<Arc<dyn factor_sqlite::ConnectionPool>> {
let _ = (database_kind, config);
todo!()
Ok(Arc::new(InvalidConnectionPool))
}

fn default(&self, label: &str) -> Option<Arc<dyn factor_sqlite::ConnectionPool>> {
let _ = label;
todo!()
let Some(default) = &self.default else {
return None;
};
(default == label).then_some(Arc::new(InvalidConnectionPool))
}
}

/// A connection pool that always returns an error.
struct InvalidConnectionPool;

#[async_trait::async_trait]
impl factor_sqlite::ConnectionPool for InvalidConnectionPool {
async fn get_connection(
&self,
) -> Result<Arc<dyn factor_sqlite::Connection + 'static>, spin_world::v2::sqlite::Error> {
Err(spin_world::v2::sqlite::Error::InvalidConnection)
}
}

0 comments on commit 0a9f22a

Please sign in to comment.