Skip to content
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
merged 1 commit into from
Jul 23, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
2 changes: 2 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 3 additions & 0 deletions crates/factor-variables/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,14 @@ authors = { workspace = true }
edition = { workspace = true }

[dependencies]
dotenvy = "0.15"
serde = { version = "1.0", features = ["rc"] }
spin-expressions = { path = "../expressions" }
spin-factors = { path = "../factors" }
spin-world = { path = "../world" }
tokio = { version = "1", features = ["rt-multi-thread"] }
toml = "0.8"
tracing = { workspace = true }

[dev-dependencies]
spin-factors-test = { path = "../factors-test" }
Expand Down
10 changes: 6 additions & 4 deletions crates/factor-variables/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
mod provider;
pub mod provider;

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

use provider::{provider_from_toml_fn, ProviderFromToml};
use serde::Deserialize;
use spin_expressions::ProviderResolver;
use spin_factors::{
Expand All @@ -16,7 +15,7 @@ pub use provider::{MakeVariablesProvider, StaticVariables};

#[derive(Default)]
pub struct VariablesFactor {
provider_types: HashMap<&'static str, ProviderFromToml>,
provider_types: HashMap<&'static str, provider::ProviderFromToml>,
}

impl VariablesFactor {
Expand All @@ -26,7 +25,10 @@ impl VariablesFactor {
) -> anyhow::Result<()> {
if self
.provider_types
.insert(T::RUNTIME_CONFIG_TYPE, provider_from_toml_fn(provider_type))
.insert(
T::RUNTIME_CONFIG_TYPE,
provider::provider_from_toml_fn(provider_type),
)
.is_some()
{
bail!("duplicate provider type {:?}", T::RUNTIME_CONFIG_TYPE);
Expand Down
35 changes: 7 additions & 28 deletions crates/factor-variables/src/provider.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
use std::{collections::HashMap, sync::Arc};
mod env;
mod statik;

use serde::{de::DeserializeOwned, Deserialize};
use spin_expressions::{async_trait::async_trait, Key, Provider};
pub use env::EnvVariables;
pub use statik::StaticVariables;

use serde::de::DeserializeOwned;
use spin_expressions::Provider;
use spin_factors::anyhow;

pub trait MakeVariablesProvider: 'static {
Expand All @@ -24,28 +28,3 @@ pub(crate) fn provider_from_toml_fn<T: MakeVariablesProvider>(
Ok(Box::new(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)
}
}

#[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())
}
}
215 changes: 215 additions & 0 deletions crates/factor-variables/src/provider/env.rs
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,
Copy link
Collaborator

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...

Copy link
Collaborator Author

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.

Copy link
Collaborator

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? 🤷

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
);
}
}
34 changes: 34 additions & 0 deletions crates/factor-variables/src/provider/statik.rs
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())
}
}
Loading