fnox_core/providers/
resolver.rs1use crate::config::Config;
10use crate::env;
11use crate::error::{FnoxError, Result};
12use crate::suggest::{find_similar, format_suggestions};
13use std::collections::HashSet;
14
15use super::secret_ref::{OptionStringOrSecretRef, StringOrSecretRef};
16use super::{ProviderConfig, ResolvedProviderConfig};
17
18pub struct ResolutionContext {
21 provider_stack: HashSet<String>,
23 resolution_path: Vec<String>,
25}
26
27impl ResolutionContext {
28 pub fn new() -> Self {
30 Self {
31 provider_stack: HashSet::new(),
32 resolution_path: Vec::new(),
33 }
34 }
35
36 fn is_resolving(&self, provider_name: &str) -> bool {
38 self.provider_stack.contains(provider_name)
39 }
40
41 pub fn push(&mut self, provider_name: &str) {
43 self.provider_stack.insert(provider_name.to_string());
44 self.resolution_path.push(provider_name.to_string());
45 }
46
47 pub fn pop(&mut self) {
49 if let Some(provider_name) = self.resolution_path.pop() {
50 self.provider_stack.remove(&provider_name);
51 }
52 }
53
54 fn path_string(&self) -> String {
56 self.resolution_path.join(" -> ")
57 }
58}
59
60impl Default for ResolutionContext {
61 fn default() -> Self {
62 Self::new()
63 }
64}
65
66pub async fn resolve_provider_config(
80 config: &Config,
81 profile: &str,
82 provider_name: &str,
83 provider_config: &ProviderConfig,
84) -> Result<ResolvedProviderConfig> {
85 let mut ctx = ResolutionContext::new();
86 resolve_provider_config_with_context(config, profile, provider_name, provider_config, &mut ctx)
87 .await
88}
89
90pub fn resolve_provider_config_with_context<'a>(
92 config: &'a Config,
93 profile: &'a str,
94 provider_name: &'a str,
95 provider_config: &'a ProviderConfig,
96 ctx: &'a mut ResolutionContext,
97) -> std::pin::Pin<Box<dyn std::future::Future<Output = Result<ResolvedProviderConfig>> + Send + 'a>>
98{
99 Box::pin(async move {
100 if ctx.is_resolving(provider_name) {
102 return Err(FnoxError::ProviderConfigCycle {
103 provider: provider_name.to_string(),
104 cycle: format!("{} -> {}", ctx.path_string(), provider_name),
105 });
106 }
107
108 ctx.push(provider_name);
110
111 let result = super::generated::providers_resolver::resolve_provider_config_match(
113 config,
114 profile,
115 provider_name,
116 provider_config,
117 ctx,
118 )
119 .await;
120
121 ctx.pop();
123
124 result
125 })
126}
127
128pub fn resolve_required<'a>(
130 config: &'a Config,
131 profile: &'a str,
132 provider_name: &'a str,
133 _field_name: &'a str,
134 value: &'a StringOrSecretRef,
135 ctx: &'a mut ResolutionContext,
136) -> std::pin::Pin<Box<dyn std::future::Future<Output = Result<String>> + Send + 'a>> {
137 Box::pin(async move {
138 match value {
139 StringOrSecretRef::Literal(s) => Ok(s.clone()),
140 StringOrSecretRef::SecretRef { secret } => {
141 resolve_secret_ref(config, profile, provider_name, secret, ctx).await
142 }
143 }
144 })
145}
146
147pub fn resolve_option<'a>(
149 config: &'a Config,
150 profile: &'a str,
151 provider_name: &'a str,
152 value: &'a OptionStringOrSecretRef,
153 ctx: &'a mut ResolutionContext,
154) -> std::pin::Pin<Box<dyn std::future::Future<Output = Result<Option<String>>> + Send + 'a>> {
155 Box::pin(async move {
156 match value.as_ref() {
157 None => Ok(None),
158 Some(StringOrSecretRef::Literal(s)) => Ok(Some(s.clone())),
159 Some(StringOrSecretRef::SecretRef { secret }) => {
160 let resolved =
161 resolve_secret_ref(config, profile, provider_name, secret, ctx).await?;
162 Ok(Some(resolved))
163 }
164 }
165 })
166}
167
168fn resolve_secret_ref<'a>(
174 config: &'a Config,
175 profile: &'a str,
176 provider_name: &'a str,
177 secret_name: &'a str,
178 ctx: &'a mut ResolutionContext,
179) -> std::pin::Pin<Box<dyn std::future::Future<Output = Result<String>> + Send + 'a>> {
180 Box::pin(async move {
181 let secrets = config.get_secrets(profile).unwrap_or_default();
183
184 if let Some(secret_config) = secrets.get(secret_name) {
185 if let Some(secret_provider_name) = secret_config.provider()
187 && let Some(provider_value) = secret_config.value()
188 {
189 let providers = config.get_providers(profile);
191 if let Some(secret_provider_config) = providers.get(secret_provider_name) {
192 if env::is_non_interactive()
193 && secret_provider_config.requires_interactive_auth()
194 {
195 return Err(FnoxError::Provider(format!(
196 "Provider '{}' requires interactive authentication and cannot be used in non-interactive mode. Use 'fnox exec' instead.",
197 secret_provider_name
198 )));
199 }
200
201 let resolved_provider = resolve_provider_config_with_context(
203 config,
204 profile,
205 secret_provider_name,
206 secret_provider_config,
207 ctx,
208 )
209 .await?;
210
211 let provider = super::get_provider_from_resolved(
213 secret_provider_name,
214 &resolved_provider,
215 )?;
216 return provider.get_secret(provider_value).await;
217 } else {
218 let available_providers: Vec<_> =
220 providers.keys().map(|s| s.as_str()).collect();
221 let similar = find_similar(secret_provider_name, available_providers);
222 let suggestion = format_suggestions(&similar);
223
224 return Err(FnoxError::ProviderNotConfigured {
225 provider: secret_provider_name.to_string(),
226 profile: profile.to_string(),
227 config_path: config.provider_sources.get(secret_provider_name).cloned(),
228 suggestion,
229 });
230 }
231 }
232
233 if let Some(ref default) = secret_config.default {
235 return Ok(default.clone());
236 }
237 }
238
239 env::var(secret_name).map_err(|_| FnoxError::ProviderConfigResolutionFailed {
241 provider: provider_name.to_string(),
242 secret: secret_name.to_string(),
243 details: format!(
244 "Secret '{}' not found in config or environment",
245 secret_name
246 ),
247 })
248 })
249}
250
251#[cfg(test)]
252mod tests {
253 use super::*;
254
255 #[test]
256 fn test_resolution_context_cycle_detection() {
257 let mut ctx = ResolutionContext::new();
258
259 assert!(!ctx.is_resolving("provider_a"));
260
261 ctx.push("provider_a");
262 assert!(ctx.is_resolving("provider_a"));
263 assert!(!ctx.is_resolving("provider_b"));
264
265 ctx.push("provider_b");
266 assert!(ctx.is_resolving("provider_a"));
267 assert!(ctx.is_resolving("provider_b"));
268
269 ctx.pop();
270 assert!(ctx.is_resolving("provider_a"));
271 assert!(!ctx.is_resolving("provider_b"));
272
273 ctx.pop();
274 assert!(!ctx.is_resolving("provider_a"));
275 }
276
277 #[test]
278 fn test_resolution_path() {
279 let mut ctx = ResolutionContext::new();
280
281 ctx.push("a");
282 ctx.push("b");
283 ctx.push("c");
284
285 assert_eq!(ctx.path_string(), "a -> b -> c");
286 }
287}