Skip to content

Commit cdff737

Browse files
committed
Migrate spin-config to new core
Signed-off-by: Lann Martin <lann.martin@fermyon.com>
1 parent 86a7199 commit cdff737

File tree

8 files changed

+143
-137
lines changed

8 files changed

+143
-137
lines changed

Cargo.lock

Lines changed: 4 additions & 3 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

crates/config/Cargo.toml

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,15 +7,17 @@ authors = [ "Fermyon Engineering <engineering@fermyon.com>" ]
77
[dependencies]
88
anyhow = "1.0"
99
async-trait = "0.1"
10-
spin-engine = { path = "../engine" }
11-
spin-manifest = { path = "../manifest" }
10+
dotenvy = "0.15"
11+
once_cell = "1"
12+
spin-app = { path = "../app" }
13+
spin-core = { path = "../core" }
1214
thiserror = "1"
15+
tokio = { version = "1", features = ["rt-multi-thread"] }
1316

1417
[dependencies.wit-bindgen-wasmtime]
1518
git = "https://github.com/bytecodealliance/wit-bindgen"
1619
rev = "cb871cfa1ee460b51eb1d144b175b9aab9c50aba"
1720
features = ["async"]
1821

1922
[dev-dependencies]
20-
tokio = { version = "1", features = [ "rt-multi-thread" ] }
2123
toml = "0.5"

crates/config/src/host_component.rs

Lines changed: 52 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,60 +1,87 @@
1-
use std::sync::Arc;
1+
use std::sync::{Arc, Mutex};
22

3-
use spin_engine::host_component::HostComponent;
4-
use spin_manifest::CoreComponent;
5-
use wit_bindgen_wasmtime::async_trait;
3+
use async_trait::async_trait;
4+
use once_cell::sync::OnceCell;
5+
use spin_app::{AppComponent, DynamicHostComponent};
6+
use spin_core::HostComponent;
67

7-
use crate::{Error, Key, Resolver};
8+
use crate::{Error, Key, Provider, Resolver};
89

9-
mod wit {
10-
wit_bindgen_wasmtime::export!({paths: ["../../wit/ephemeral/spin-config.wit"], async: *});
11-
}
10+
wit_bindgen_wasmtime::export!({paths: ["../../wit/ephemeral/spin-config.wit"], async: *});
1211

1312
pub struct ConfigHostComponent {
14-
resolver: Arc<Resolver>,
13+
providers: Mutex<Vec<Box<dyn Provider>>>,
14+
resolver: Arc<OnceCell<Resolver>>,
1515
}
1616

1717
impl ConfigHostComponent {
18-
pub fn new(resolver: Resolver) -> Self {
18+
pub fn new(providers: Vec<Box<dyn Provider>>) -> Self {
1919
Self {
20-
resolver: Arc::new(resolver),
20+
providers: Mutex::new(providers),
21+
resolver: Default::default(),
2122
}
2223
}
2324
}
2425

2526
impl HostComponent for ConfigHostComponent {
26-
type State = ComponentConfig;
27+
type Data = ComponentConfig;
2728

2829
fn add_to_linker<T: Send>(
29-
linker: &mut wit_bindgen_wasmtime::wasmtime::Linker<spin_engine::RuntimeContext<T>>,
30-
state_handle: spin_engine::host_component::HostComponentsStateHandle<Self::State>,
30+
linker: &mut spin_core::Linker<T>,
31+
get: impl Fn(&mut spin_core::Data<T>) -> &mut Self::Data + Send + Sync + Copy + 'static,
3132
) -> anyhow::Result<()> {
32-
wit::spin_config::add_to_linker(linker, move |ctx| state_handle.get_mut(ctx))
33+
spin_config::add_to_linker(linker, get)
3334
}
3435

35-
fn build_state(&self, component: &CoreComponent) -> anyhow::Result<Self::State> {
36-
Ok(ComponentConfig {
37-
component_id: component.id.clone(),
36+
fn build_data(&self) -> Self::Data {
37+
ComponentConfig {
3838
resolver: self.resolver.clone(),
39-
})
39+
component_id: None,
40+
}
41+
}
42+
}
43+
44+
impl DynamicHostComponent for ConfigHostComponent {
45+
fn update_data(&self, data: &mut Self::Data, component: &AppComponent) -> anyhow::Result<()> {
46+
self.resolver.get_or_try_init(|| {
47+
let mut resolver = Resolver::new(
48+
component
49+
.app
50+
.variables()
51+
.map(|(key, var)| (key.clone(), var.clone())),
52+
)?;
53+
for provider in self.providers.lock().unwrap().drain(..) {
54+
resolver.add_provider(provider);
55+
}
56+
Ok::<_, anyhow::Error>(resolver)
57+
})?;
58+
data.component_id = Some(component.id().to_string());
59+
Ok(())
4060
}
4161
}
4262

4363
/// A component configuration interface implementation.
4464
pub struct ComponentConfig {
45-
component_id: String,
46-
resolver: Arc<Resolver>,
65+
resolver: Arc<OnceCell<Resolver>>,
66+
component_id: Option<String>,
4767
}
4868

4969
#[async_trait]
50-
impl wit::spin_config::SpinConfig for ComponentConfig {
51-
async fn get_config(&mut self, key: &str) -> Result<String, wit::spin_config::Error> {
70+
impl spin_config::SpinConfig for ComponentConfig {
71+
async fn get_config(&mut self, key: &str) -> Result<String, spin_config::Error> {
72+
// Set by DynamicHostComponent::update_data
73+
let component_id = self.component_id.as_deref().unwrap();
5274
let key = Key::new(key)?;
53-
Ok(self.resolver.resolve(&self.component_id, key).await?)
75+
Ok(self
76+
.resolver
77+
.get()
78+
.unwrap()
79+
.resolve(component_id, key)
80+
.await?)
5481
}
5582
}
5683

57-
impl From<Error> for wit::spin_config::Error {
84+
impl From<Error> for spin_config::Error {
5885
fn from(err: Error) -> Self {
5986
match err {
6087
Error::InvalidKey(msg) => Self::InvalidKey(msg),

crates/config/src/lib.rs

Lines changed: 14 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,21 @@
1-
pub mod host_component;
1+
mod host_component;
22
pub mod provider;
3-
43
mod template;
54

65
use std::{borrow::Cow, collections::HashMap, fmt::Debug};
76

8-
pub use async_trait::async_trait;
7+
use spin_app::Variable;
98

10-
pub use provider::Provider;
11-
use spin_manifest::Variable;
9+
pub use crate::{host_component::ConfigHostComponent, provider::Provider};
1210
use template::{Part, Template};
1311

14-
type Result<T> = std::result::Result<T, Error>;
15-
1612
/// A configuration resolver.
1713
#[derive(Debug, Default)]
1814
pub struct Resolver {
1915
// variable key -> variable
2016
variables: HashMap<String, Variable>,
2117
// component ID -> config key -> config value template
22-
components_configs: HashMap<String, HashMap<String, Template>>,
18+
component_configs: HashMap<String, HashMap<String, Template>>,
2319
providers: Vec<Box<dyn Provider>>,
2420
}
2521

@@ -31,7 +27,7 @@ impl Resolver {
3127
variables.keys().try_for_each(|key| Key::validate(key))?;
3228
Ok(Self {
3329
variables,
34-
components_configs: Default::default(),
30+
component_configs: Default::default(),
3531
providers: Default::default(),
3632
})
3733
}
@@ -53,19 +49,19 @@ impl Resolver {
5349
})
5450
.collect::<Result<_>>()?;
5551

56-
self.components_configs.insert(component_id, templates);
52+
self.component_configs.insert(component_id, templates);
5753

5854
Ok(())
5955
}
6056

6157
/// Adds a config Provider to the Resolver.
62-
pub fn add_provider(&mut self, provider: impl Provider + 'static) {
63-
self.providers.push(Box::new(provider));
58+
pub fn add_provider(&mut self, provider: Box<dyn Provider>) {
59+
self.providers.push(provider);
6460
}
6561

6662
/// Resolves a config value for the given path.
6763
pub async fn resolve(&self, component_id: &str, key: Key<'_>) -> Result<String> {
68-
let configs = self.components_configs.get(component_id).ok_or_else(|| {
64+
let configs = self.component_configs.get(component_id).ok_or_else(|| {
6965
Error::UnknownPath(format!("no config for component {component_id:?}"))
7066
})?;
7167

@@ -165,6 +161,8 @@ impl<'a> AsRef<str> for Key<'a> {
165161
}
166162
}
167163

164+
type Result<T> = std::result::Result<T, Error>;
165+
168166
/// A config resolution error.
169167
#[derive(Debug, thiserror::Error)]
170168
pub enum Error {
@@ -195,6 +193,8 @@ pub enum Error {
195193

196194
#[cfg(test)]
197195
mod tests {
196+
use async_trait::async_trait;
197+
198198
use super::*;
199199

200200
#[derive(Debug)]
@@ -235,7 +235,7 @@ mod tests {
235235
[("test_key".into(), config_template.into())],
236236
)
237237
.unwrap();
238-
resolver.add_provider(TestProvider);
238+
resolver.add_provider(Box::new(TestProvider));
239239
resolver.resolve("test-component", Key("test_key")).await
240240
}
241241

crates/config/src/provider/env.rs

Lines changed: 49 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -1,59 +1,73 @@
1-
use std::collections::HashMap;
1+
use std::{collections::HashMap, path::PathBuf, sync::Mutex};
22

3-
use anyhow::Context;
3+
use anyhow::{Context, Result};
44
use async_trait::async_trait;
55

66
use crate::{Key, Provider};
77

8-
pub const DEFAULT_PREFIX: &str = "SPIN_APP";
9-
108
/// A config Provider that uses environment variables.
119
#[derive(Debug)]
1210
pub struct EnvProvider {
1311
prefix: String,
14-
envs: HashMap<String, String>,
12+
dotenv_path: Option<PathBuf>,
13+
dotenv_cache: Mutex<Option<HashMap<String, String>>>,
1514
}
1615

1716
impl EnvProvider {
1817
/// Creates a new EnvProvider.
19-
pub fn new(prefix: impl Into<String>, envs: HashMap<String, String>) -> Self {
18+
pub fn new(prefix: impl Into<String>, dotenv_path: Option<PathBuf>) -> Self {
2019
Self {
2120
prefix: prefix.into(),
22-
envs,
21+
dotenv_path,
22+
dotenv_cache: Default::default(),
2323
}
2424
}
2525

26-
fn get_sync(&self, key: &Key) -> anyhow::Result<Option<String>> {
26+
fn get_sync(&self, key: &Key) -> Result<Option<String>> {
2727
let env_key = format!("{}_{}", &self.prefix, key.as_ref().to_ascii_uppercase());
2828
match std::env::var(&env_key) {
29-
Err(std::env::VarError::NotPresent) => {
30-
Ok(self.envs.get(&env_key).map(|value| value.to_string()))
31-
}
29+
Err(std::env::VarError::NotPresent) => self.get_dotenv(&env_key),
3230
other => other
3331
.map(Some)
3432
.with_context(|| format!("failed to resolve env var {}", &env_key)),
3533
}
3634
}
37-
}
3835

39-
impl Default for EnvProvider {
40-
fn default() -> Self {
41-
Self {
42-
prefix: DEFAULT_PREFIX.to_string(),
43-
envs: HashMap::new(),
36+
fn get_dotenv(&self, key: &str) -> Result<Option<String>> {
37+
if self.dotenv_path.is_none() {
38+
return Ok(None);
4439
}
40+
let mut maybe_cache = self
41+
.dotenv_cache
42+
.lock()
43+
.expect("dotenv_cache lock poisoned");
44+
let cache = match maybe_cache.as_mut() {
45+
Some(cache) => cache,
46+
None => maybe_cache.insert(self.load_dotenv()?),
47+
};
48+
Ok(cache.get(key).cloned())
49+
}
50+
51+
fn load_dotenv(&self) -> Result<HashMap<String, String>> {
52+
let path = self.dotenv_path.as_deref().unwrap();
53+
Ok(dotenvy::from_path_iter(path)
54+
.into_iter()
55+
.flatten()
56+
.collect::<Result<HashMap<String, String>, _>>()?)
4557
}
4658
}
4759

4860
#[async_trait]
4961
impl Provider for EnvProvider {
50-
async fn get(&self, key: &Key) -> anyhow::Result<Option<String>> {
51-
self.get_sync(key)
62+
async fn get(&self, key: &Key) -> Result<Option<String>> {
63+
tokio::task::block_in_place(|| self.get_sync(key))
5264
}
5365
}
5466

5567
#[cfg(test)]
5668
mod test {
69+
use std::env::temp_dir;
70+
5771
use super::*;
5872

5973
#[test]
@@ -66,20 +80,22 @@ mod test {
6680
"dotenv_val".to_string(),
6781
);
6882
assert_eq!(
69-
EnvProvider::new("TESTING_SPIN", envs.clone())
83+
EnvProvider::new("TESTING_SPIN", None)
7084
.get_sync(&key1)
7185
.unwrap(),
7286
Some("val".to_string())
7387
);
88+
}
7489

75-
let key2 = Key::new("env_key2").unwrap();
76-
envs.insert(
77-
"TESTING_SPIN_ENV_KEY2".to_string(),
78-
"dotenv_val".to_string(),
79-
);
90+
#[test]
91+
fn provider_get_dotenv() {
92+
let dotenv_path = temp_dir().join("spin-env-provider-test");
93+
std::fs::write(&dotenv_path, b"TESTING_SPIN_ENV_KEY2=dotenv_val").unwrap();
94+
95+
let key = Key::new("env_key2").unwrap();
8096
assert_eq!(
81-
EnvProvider::new("TESTING_SPIN", envs.clone())
82-
.get_sync(&key2)
97+
EnvProvider::new("TESTING_SPIN", Some(dotenv_path))
98+
.get_sync(&key)
8399
.unwrap(),
84100
Some("dotenv_val".to_string())
85101
);
@@ -88,6 +104,11 @@ mod test {
88104
#[test]
89105
fn provider_get_missing() {
90106
let key = Key::new("please_do_not_ever_set_this_during_tests").unwrap();
91-
assert_eq!(EnvProvider::default().get_sync(&key).unwrap(), None);
107+
assert_eq!(
108+
EnvProvider::new("TESTING_SPIN", Default::default())
109+
.get_sync(&key)
110+
.unwrap(),
111+
None
112+
);
92113
}
93114
}

0 commit comments

Comments
 (0)