use crate::config::Config;
use crate::env;
use crate::error::{FnoxError, Result};
use crate::suggest::{find_similar, format_suggestions};
use std::collections::HashSet;
use super::secret_ref::{OptionStringOrSecretRef, StringOrSecretRef};
use super::{ProviderConfig, ResolvedProviderConfig};
pub struct ResolutionContext {
provider_stack: HashSet<String>,
resolution_path: Vec<String>,
}
impl ResolutionContext {
pub fn new() -> Self {
Self {
provider_stack: HashSet::new(),
resolution_path: Vec::new(),
}
}
fn is_resolving(&self, provider_name: &str) -> bool {
self.provider_stack.contains(provider_name)
}
pub fn push(&mut self, provider_name: &str) {
self.provider_stack.insert(provider_name.to_string());
self.resolution_path.push(provider_name.to_string());
}
pub fn pop(&mut self) {
if let Some(provider_name) = self.resolution_path.pop() {
self.provider_stack.remove(&provider_name);
}
}
fn path_string(&self) -> String {
self.resolution_path.join(" -> ")
}
}
impl Default for ResolutionContext {
fn default() -> Self {
Self::new()
}
}
pub async fn resolve_provider_config(
config: &Config,
profile: &str,
provider_name: &str,
provider_config: &ProviderConfig,
) -> Result<ResolvedProviderConfig> {
let mut ctx = ResolutionContext::new();
resolve_provider_config_with_context(config, profile, provider_name, provider_config, &mut ctx)
.await
}
pub fn resolve_provider_config_with_context<'a>(
config: &'a Config,
profile: &'a str,
provider_name: &'a str,
provider_config: &'a ProviderConfig,
ctx: &'a mut ResolutionContext,
) -> std::pin::Pin<Box<dyn std::future::Future<Output = Result<ResolvedProviderConfig>> + Send + 'a>>
{
Box::pin(async move {
if ctx.is_resolving(provider_name) {
return Err(FnoxError::ProviderConfigCycle {
provider: provider_name.to_string(),
cycle: format!("{} -> {}", ctx.path_string(), provider_name),
});
}
ctx.push(provider_name);
let result = super::generated::providers_resolver::resolve_provider_config_match(
config,
profile,
provider_name,
provider_config,
ctx,
)
.await;
ctx.pop();
result
})
}
pub fn resolve_required<'a>(
config: &'a Config,
profile: &'a str,
provider_name: &'a str,
_field_name: &'a str,
value: &'a StringOrSecretRef,
ctx: &'a mut ResolutionContext,
) -> std::pin::Pin<Box<dyn std::future::Future<Output = Result<String>> + Send + 'a>> {
Box::pin(async move {
match value {
StringOrSecretRef::Literal(s) => Ok(s.clone()),
StringOrSecretRef::SecretRef { secret } => {
resolve_secret_ref(config, profile, provider_name, secret, ctx).await
}
}
})
}
pub fn resolve_option<'a>(
config: &'a Config,
profile: &'a str,
provider_name: &'a str,
value: &'a OptionStringOrSecretRef,
ctx: &'a mut ResolutionContext,
) -> std::pin::Pin<Box<dyn std::future::Future<Output = Result<Option<String>>> + Send + 'a>> {
Box::pin(async move {
match value.as_ref() {
None => Ok(None),
Some(StringOrSecretRef::Literal(s)) => Ok(Some(s.clone())),
Some(StringOrSecretRef::SecretRef { secret }) => {
let resolved =
resolve_secret_ref(config, profile, provider_name, secret, ctx).await?;
Ok(Some(resolved))
}
}
})
}
fn resolve_secret_ref<'a>(
config: &'a Config,
profile: &'a str,
provider_name: &'a str,
secret_name: &'a str,
ctx: &'a mut ResolutionContext,
) -> std::pin::Pin<Box<dyn std::future::Future<Output = Result<String>> + Send + 'a>> {
Box::pin(async move {
let secrets = config.get_secrets(profile).unwrap_or_default();
if let Some(secret_config) = secrets.get(secret_name) {
if let Some(secret_provider_name) = secret_config.provider()
&& let Some(provider_value) = secret_config.value()
{
let providers = config.get_providers(profile);
if let Some(secret_provider_config) = providers.get(secret_provider_name) {
if env::is_non_interactive()
&& secret_provider_config.requires_interactive_auth()
{
return Err(FnoxError::Provider(format!(
"Provider '{}' requires interactive authentication and cannot be used in non-interactive mode. Use 'fnox exec' instead.",
secret_provider_name
)));
}
let resolved_provider = resolve_provider_config_with_context(
config,
profile,
secret_provider_name,
secret_provider_config,
ctx,
)
.await?;
let provider = super::get_provider_from_resolved(
secret_provider_name,
&resolved_provider,
)?;
return provider.get_secret(provider_value).await;
} else {
let available_providers: Vec<_> =
providers.keys().map(|s| s.as_str()).collect();
let similar = find_similar(secret_provider_name, available_providers);
let suggestion = format_suggestions(&similar);
return Err(FnoxError::ProviderNotConfigured {
provider: secret_provider_name.to_string(),
profile: profile.to_string(),
config_path: config.provider_sources.get(secret_provider_name).cloned(),
suggestion,
});
}
}
if let Some(ref default) = secret_config.default {
return Ok(default.clone());
}
}
env::var(secret_name).map_err(|_| FnoxError::ProviderConfigResolutionFailed {
provider: provider_name.to_string(),
secret: secret_name.to_string(),
details: format!(
"Secret '{}' not found in config or environment",
secret_name
),
})
})
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_resolution_context_cycle_detection() {
let mut ctx = ResolutionContext::new();
assert!(!ctx.is_resolving("provider_a"));
ctx.push("provider_a");
assert!(ctx.is_resolving("provider_a"));
assert!(!ctx.is_resolving("provider_b"));
ctx.push("provider_b");
assert!(ctx.is_resolving("provider_a"));
assert!(ctx.is_resolving("provider_b"));
ctx.pop();
assert!(ctx.is_resolving("provider_a"));
assert!(!ctx.is_resolving("provider_b"));
ctx.pop();
assert!(!ctx.is_resolving("provider_a"));
}
#[test]
fn test_resolution_path() {
let mut ctx = ResolutionContext::new();
ctx.push("a");
ctx.push("b");
ctx.push("c");
assert_eq!(ctx.path_string(), "a -> b -> c");
}
}