-
Notifications
You must be signed in to change notification settings - Fork 261
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
Add environment variables provider #2670
Merged
Changes from all commits
Commits
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,215 @@ | ||
use std::{ | ||
collections::HashMap, | ||
env::VarError, | ||
path::{Path, PathBuf}, | ||
sync::OnceLock, | ||
}; | ||
|
||
use serde::Deserialize; | ||
use spin_expressions::{Key, Provider}; | ||
use spin_factors::anyhow::{self, Context as _}; | ||
use spin_world::async_trait; | ||
use tracing::{instrument, Level}; | ||
|
||
use crate::MakeVariablesProvider; | ||
|
||
/// Creator of a environment variables provider. | ||
pub struct EnvVariables; | ||
|
||
impl MakeVariablesProvider for EnvVariables { | ||
const RUNTIME_CONFIG_TYPE: &'static str = "env"; | ||
|
||
type RuntimeConfig = EnvVariablesConfig; | ||
type Provider = EnvVariablesProvider; | ||
|
||
fn make_provider(&self, runtime_config: Self::RuntimeConfig) -> anyhow::Result<Self::Provider> { | ||
Ok(EnvVariablesProvider::new( | ||
runtime_config.prefix, | ||
|key| std::env::var(key), | ||
runtime_config.dotenv_path, | ||
)) | ||
} | ||
} | ||
|
||
/// Configuration for the environment variables provider. | ||
#[derive(Debug, Default, Deserialize)] | ||
#[serde(deny_unknown_fields)] | ||
pub struct EnvVariablesConfig { | ||
/// A prefix to add to variable names when resolving from the environment. | ||
/// | ||
/// Unless empty, joined to the variable name with an underscore. | ||
#[serde(default)] | ||
pub prefix: Option<String>, | ||
/// Optional path to a 'dotenv' file which will be merged into the environment. | ||
#[serde(default)] | ||
pub dotenv_path: Option<PathBuf>, | ||
} | ||
|
||
const DEFAULT_ENV_PREFIX: &str = "SPIN_VARIABLE"; | ||
|
||
/// A config Provider that uses environment variables. | ||
pub struct EnvVariablesProvider { | ||
prefix: Option<String>, | ||
env_fetcher: Box<dyn Fn(&str) -> Result<String, VarError> + Send + Sync>, | ||
dotenv_path: Option<PathBuf>, | ||
dotenv_cache: OnceLock<HashMap<String, String>>, | ||
} | ||
|
||
impl EnvVariablesProvider { | ||
/// Creates a new EnvProvider. | ||
/// | ||
/// * `prefix` - The string prefix to use to distinguish an environment variable that should be used. | ||
/// If not set, the default prefix is used. | ||
/// * `env_fetcher` - The function to use to fetch an environment variable. | ||
/// * `dotenv_path` - The path to the .env file to load environment variables from. If not set, | ||
/// no .env file is loaded. | ||
pub fn new( | ||
prefix: Option<impl Into<String>>, | ||
env_fetcher: impl Fn(&str) -> Result<String, VarError> + Send + Sync + 'static, | ||
dotenv_path: Option<PathBuf>, | ||
) -> Self { | ||
Self { | ||
prefix: prefix.map(Into::into), | ||
dotenv_path, | ||
env_fetcher: Box::new(env_fetcher), | ||
dotenv_cache: Default::default(), | ||
} | ||
} | ||
|
||
/// Gets the value of a variable from the environment. | ||
fn get_sync(&self, key: &Key) -> anyhow::Result<Option<String>> { | ||
let prefix = self | ||
.prefix | ||
.clone() | ||
.unwrap_or(DEFAULT_ENV_PREFIX.to_string()); | ||
|
||
let upper_key = key.as_ref().to_ascii_uppercase(); | ||
let env_key = format!("{prefix}_{upper_key}"); | ||
|
||
self.query_env(&env_key) | ||
} | ||
|
||
/// Queries the environment for a variable defaulting to dotenv. | ||
fn query_env(&self, env_key: &str) -> anyhow::Result<Option<String>> { | ||
match (self.env_fetcher)(env_key) { | ||
Err(std::env::VarError::NotPresent) => self.get_dotenv(env_key), | ||
other => other | ||
.map(Some) | ||
.with_context(|| format!("failed to resolve env var {env_key}")), | ||
} | ||
} | ||
|
||
fn get_dotenv(&self, key: &str) -> anyhow::Result<Option<String>> { | ||
let Some(dotenv_path) = self.dotenv_path.as_deref() else { | ||
return Ok(None); | ||
}; | ||
let cache = match self.dotenv_cache.get() { | ||
Some(cache) => cache, | ||
None => { | ||
let cache = load_dotenv(dotenv_path)?; | ||
let _ = self.dotenv_cache.set(cache); | ||
// Safe to unwrap because we just set the cache. | ||
// Ensures we always get the first value set. | ||
self.dotenv_cache.get().unwrap() | ||
} | ||
}; | ||
Ok(cache.get(key).cloned()) | ||
} | ||
} | ||
|
||
impl std::fmt::Debug for EnvVariablesProvider { | ||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { | ||
f.debug_struct("EnvProvider") | ||
.field("prefix", &self.prefix) | ||
.field("dotenv_path", &self.dotenv_path) | ||
.finish() | ||
} | ||
} | ||
|
||
fn load_dotenv(dotenv_path: &Path) -> anyhow::Result<HashMap<String, String>> { | ||
Ok(dotenvy::from_path_iter(dotenv_path) | ||
.into_iter() | ||
.flatten() | ||
.collect::<Result<HashMap<String, String>, _>>()?) | ||
} | ||
|
||
#[async_trait] | ||
impl Provider for EnvVariablesProvider { | ||
#[instrument(name = "spin_variables.get_from_env", skip(self), err(level = Level::INFO))] | ||
async fn get(&self, key: &Key) -> anyhow::Result<Option<String>> { | ||
tokio::task::block_in_place(|| self.get_sync(key)) | ||
} | ||
} | ||
|
||
#[cfg(test)] | ||
mod test { | ||
use std::env::temp_dir; | ||
|
||
use super::*; | ||
|
||
struct TestEnv { | ||
map: HashMap<String, String>, | ||
} | ||
|
||
impl TestEnv { | ||
fn new() -> Self { | ||
Self { | ||
map: Default::default(), | ||
} | ||
} | ||
|
||
fn insert(&mut self, key: &str, value: &str) { | ||
self.map.insert(key.to_string(), value.to_string()); | ||
} | ||
|
||
fn get(&self, key: &str) -> Result<String, VarError> { | ||
self.map.get(key).cloned().ok_or(VarError::NotPresent) | ||
} | ||
} | ||
|
||
#[test] | ||
fn provider_get() { | ||
let mut env = TestEnv::new(); | ||
env.insert("TESTING_SPIN_ENV_KEY1", "val"); | ||
let key1 = Key::new("env_key1").unwrap(); | ||
assert_eq!( | ||
EnvVariablesProvider::new(Some("TESTING_SPIN"), move |key| env.get(key), None) | ||
.get_sync(&key1) | ||
.unwrap(), | ||
Some("val".to_string()) | ||
); | ||
} | ||
|
||
#[test] | ||
fn provider_get_dotenv() { | ||
let dotenv_path = temp_dir().join("spin-env-provider-test"); | ||
std::fs::write(&dotenv_path, b"TESTING_SPIN_ENV_KEY2=dotenv_val").unwrap(); | ||
|
||
let key = Key::new("env_key2").unwrap(); | ||
assert_eq!( | ||
EnvVariablesProvider::new( | ||
Some("TESTING_SPIN"), | ||
|_| Err(VarError::NotPresent), | ||
Some(dotenv_path) | ||
) | ||
.get_sync(&key) | ||
.unwrap(), | ||
Some("dotenv_val".to_string()) | ||
); | ||
} | ||
|
||
#[test] | ||
fn provider_get_missing() { | ||
let key = Key::new("definitely_not_set").unwrap(); | ||
assert_eq!( | ||
EnvVariablesProvider::new( | ||
Some("TESTING_SPIN"), | ||
|_| Err(VarError::NotPresent), | ||
Default::default() | ||
) | ||
.get_sync(&key) | ||
.unwrap(), | ||
None | ||
); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,34 @@ | ||
use std::{collections::HashMap, sync::Arc}; | ||
|
||
use serde::Deserialize; | ||
use spin_expressions::{async_trait::async_trait, Key, Provider}; | ||
use spin_factors::anyhow; | ||
|
||
use crate::MakeVariablesProvider; | ||
|
||
/// Creator of a static variables provider. | ||
pub struct StaticVariables; | ||
|
||
impl MakeVariablesProvider for StaticVariables { | ||
const RUNTIME_CONFIG_TYPE: &'static str = "static"; | ||
|
||
type RuntimeConfig = StaticVariablesProvider; | ||
type Provider = StaticVariablesProvider; | ||
|
||
fn make_provider(&self, runtime_config: Self::RuntimeConfig) -> anyhow::Result<Self::Provider> { | ||
Ok(runtime_config) | ||
} | ||
} | ||
|
||
/// A variables provider that reads variables from an static map. | ||
#[derive(Debug, Deserialize)] | ||
pub struct StaticVariablesProvider { | ||
values: Arc<HashMap<String, String>>, | ||
} | ||
|
||
#[async_trait] | ||
impl Provider for StaticVariablesProvider { | ||
async fn get(&self, key: &Key) -> anyhow::Result<Option<String>> { | ||
Ok(self.values.get(key.as_str()).cloned()) | ||
} | ||
} |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do we have a concrete need for this? Seems a bit marginal vs implementing an entirely new provider...
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It makes testing not rely on the environment it's run in. If it complicates too many things in your opinion, we can go back to just assuming
std::env::variable
.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe a separate
with_env_fetcher
? 🤷