1#[cfg(feature = "env")]
2use crate::backend::env::EnvBackend;
3#[cfg(feature = "file")]
4use crate::backend::file::FileBackend;
5use crate::embedded::{CoreBuilder, MemoryBackend, MemoryKeyProvider, SecretsCore, SecretsError};
6use crate::probe;
7use crate::provider::Provider;
8use std::path::{Path, PathBuf};
9use std::time::Duration;
10
11#[derive(Debug, Clone)]
13pub struct ResolverConfig {
14 provider: Provider,
15 tenant: Option<String>,
16 team: Option<String>,
17 cache_ttl: Option<Duration>,
18 cache_capacity: Option<usize>,
19 file_root: Option<PathBuf>,
20 dev_fallback: bool,
21}
22
23impl ResolverConfig {
24 pub fn new() -> Self {
26 Self {
27 provider: Provider::Auto,
28 tenant: None,
29 team: None,
30 cache_ttl: None,
31 cache_capacity: None,
32 file_root: None,
33 dev_fallback: true,
34 }
35 }
36
37 pub fn from_env() -> Self {
44 let mut config = ResolverConfig::new();
45
46 if let Ok(provider) = std::env::var("GREENTIC_SECRETS_PROVIDER")
47 && let Some(parsed) = Provider::from_env_value(&provider)
48 {
49 config.provider = parsed;
50 }
51
52 let dev_fallback = std::env::var("GREENTIC_SECRETS_DEV")
53 .map(|value| matches!(value.as_str(), "1" | "true" | "TRUE"))
54 .unwrap_or(true);
55 config.dev_fallback = dev_fallback;
56
57 if let Ok(root) = std::env::var("GREENTIC_SECRETS_FILE_ROOT")
58 && !root.trim().is_empty()
59 {
60 config.file_root = Some(PathBuf::from(root));
61 }
62
63 config
64 }
65
66 pub fn provider(mut self, provider: Provider) -> Self {
68 self.provider = provider;
69 self
70 }
71
72 pub fn tenant(mut self, tenant: impl Into<String>) -> Self {
74 self.tenant = Some(tenant.into());
75 self
76 }
77
78 pub fn team(mut self, team: impl Into<String>) -> Self {
80 self.team = Some(team.into());
81 self
82 }
83
84 pub fn cache_ttl(mut self, ttl: Duration) -> Self {
86 self.cache_ttl = Some(ttl);
87 self
88 }
89
90 pub fn cache_capacity(mut self, capacity: usize) -> Self {
92 self.cache_capacity = Some(capacity);
93 self
94 }
95
96 pub fn file_root<P: AsRef<Path>>(mut self, root: P) -> Self {
98 self.file_root = Some(root.as_ref().to_path_buf());
99 self
100 }
101
102 pub fn dev_fallback(mut self, enabled: bool) -> Self {
104 self.dev_fallback = enabled;
105 self
106 }
107}
108
109impl Default for ResolverConfig {
110 fn default() -> Self {
111 ResolverConfig::new()
112 }
113}
114
115pub struct DefaultResolver {
117 provider: Provider,
118 core: SecretsCore,
119}
120
121impl DefaultResolver {
122 pub async fn new() -> Result<Self, SecretsError> {
124 Self::from_config(ResolverConfig::from_env()).await
125 }
126
127 pub async fn from_config(config: ResolverConfig) -> Result<Self, SecretsError> {
129 let mut builder = SecretsCore::builder();
130 builder.clear_backends();
131
132 if let Some(ref tenant) = config.tenant {
133 builder = builder.tenant(tenant.clone());
134 }
135
136 if let Some(ref team) = config.team {
137 builder = builder.team(team.clone());
138 }
139
140 if let Some(ttl) = config.cache_ttl {
141 builder = builder.default_ttl(ttl);
142 }
143
144 if let Some(capacity) = config.cache_capacity {
145 builder = builder.cache_capacity(capacity);
146 }
147
148 let requested = config.provider;
149 let selected = if let Provider::Auto = requested {
150 detect_provider().await
151 } else {
152 requested
153 };
154
155 let (builder, resolved) = configure_builder_for_provider(builder, &config, selected);
156 let core = builder.build().await?;
157
158 Ok(Self {
159 provider: resolved,
160 core,
161 })
162 }
163
164 pub fn provider(&self) -> Provider {
166 self.provider
167 }
168
169 pub fn core(&self) -> &SecretsCore {
171 &self.core
172 }
173}
174
175impl std::ops::Deref for DefaultResolver {
176 type Target = SecretsCore;
177
178 fn deref(&self) -> &Self::Target {
179 &self.core
180 }
181}
182
183async fn detect_provider() -> Provider {
184 if probe::is_kubernetes().await {
185 return Provider::K8s;
186 }
187
188 if probe::is_aws().await {
189 return Provider::Aws;
190 }
191
192 if probe::is_gcp().await {
193 return Provider::Gcp;
194 }
195
196 if probe::is_azure().await {
197 return Provider::Azure;
198 }
199
200 Provider::Local
201}
202
203fn configure_builder_for_provider(
204 builder: CoreBuilder,
205 config: &ResolverConfig,
206 requested: Provider,
207) -> (CoreBuilder, Provider) {
208 match requested {
209 Provider::Local => configure_local(builder, config),
210 Provider::Aws => configure_aws(builder, config),
211 Provider::Azure => configure_azure(builder, config),
212 Provider::Gcp => configure_gcp(builder, config),
213 Provider::K8s => configure_k8s(builder, config),
214 Provider::Auto => configure_local(builder, config),
215 }
216}
217
218fn configure_local(mut builder: CoreBuilder, config: &ResolverConfig) -> (CoreBuilder, Provider) {
219 builder = builder.backend_named("memory", MemoryBackend::new(), MemoryKeyProvider::default());
220
221 if config.dev_fallback {
222 #[cfg(feature = "env")]
223 {
224 builder = builder.backend_named("env", EnvBackend::new(), MemoryKeyProvider::default());
225 }
226 }
227
228 #[cfg(feature = "file")]
229 if let Some(root) = config.file_root.as_ref() {
230 builder = builder.backend_named(
231 "file",
232 FileBackend::new(root.clone()),
233 MemoryKeyProvider::default(),
234 );
235 }
236
237 (builder, Provider::Local)
238}
239
240fn configure_aws(builder: CoreBuilder, config: &ResolverConfig) -> (CoreBuilder, Provider) {
241 #[cfg(feature = "aws")]
242 {
243 let _ = config;
244 let builder = builder.backend_named(
245 "aws",
246 crate::backend::aws::AwsSecretsManagerBackend::new(),
247 MemoryKeyProvider::default(),
248 );
249 (builder, Provider::Aws)
250 }
251
252 #[cfg(not(feature = "aws"))]
253 {
254 tracing::warn!(
255 "aws provider requested but the `aws` feature is not enabled; falling back to local provider"
256 );
257 configure_local(builder, config)
258 }
259}
260
261fn configure_azure(builder: CoreBuilder, config: &ResolverConfig) -> (CoreBuilder, Provider) {
262 #[cfg(feature = "azure")]
263 {
264 let _ = config;
265 let builder = builder.backend_named(
266 "azure",
267 crate::backend::azure::AzureKeyVaultBackend::new(),
268 MemoryKeyProvider::default(),
269 );
270 (builder, Provider::Azure)
271 }
272
273 #[cfg(not(feature = "azure"))]
274 {
275 tracing::warn!(
276 "azure provider requested but the `azure` feature is not enabled; falling back to local provider"
277 );
278 configure_local(builder, config)
279 }
280}
281
282fn configure_gcp(builder: CoreBuilder, config: &ResolverConfig) -> (CoreBuilder, Provider) {
283 #[cfg(feature = "gcp")]
284 {
285 let _ = config;
286 let builder = builder.backend_named(
287 "gcp",
288 crate::backend::gcp::GcpSecretsManagerBackend::new(),
289 MemoryKeyProvider::default(),
290 );
291 (builder, Provider::Gcp)
292 }
293
294 #[cfg(not(feature = "gcp"))]
295 {
296 tracing::warn!(
297 "gcp provider requested but the `gcp` feature is not enabled; falling back to local provider"
298 );
299 configure_local(builder, config)
300 }
301}
302
303fn configure_k8s(builder: CoreBuilder, config: &ResolverConfig) -> (CoreBuilder, Provider) {
304 #[cfg(feature = "k8s")]
305 {
306 let _ = config;
307 let builder = builder.backend_named(
308 "k8s",
309 crate::backend::k8s::K8sBackend::new(),
310 MemoryKeyProvider::default(),
311 );
312 (builder, Provider::K8s)
313 }
314
315 #[cfg(not(feature = "k8s"))]
316 {
317 tracing::warn!(
318 "k8s provider requested but the `k8s` feature is not enabled; falling back to local provider"
319 );
320 configure_local(builder, config)
321 }
322}
323
324#[cfg(test)]
325mod tests {
326 use super::*;
327 use serde_json::{Value, json};
328
329 #[tokio::test]
330 async fn defaults_to_local_provider() {
331 let resolver = DefaultResolver::from_config(
332 ResolverConfig::new()
333 .provider(Provider::Local)
334 .tenant("example")
335 .team("core"),
336 )
337 .await
338 .expect("resolver");
339
340 assert_eq!(resolver.provider(), Provider::Local);
341
342 resolver
343 .put_json(
344 "secrets://dev/example/core/configs/api",
345 &json!({ "token": "abc" }),
346 )
347 .await
348 .expect("put");
349
350 let value: Value = resolver
351 .get_json("secrets://dev/example/core/configs/api")
352 .await
353 .expect("get");
354 assert_eq!(value["token"], "abc");
355 }
356
357 #[tokio::test]
358 async fn falls_back_when_feature_disabled() {
359 let resolver = DefaultResolver::from_config(
360 ResolverConfig::new()
361 .provider(Provider::Aws)
362 .tenant("test")
363 .team("core"),
364 )
365 .await
366 .expect("resolver");
367
368 assert!(
370 matches!(resolver.provider(), Provider::Aws | Provider::Local),
371 "resolver should either use AWS (when feature enabled) or fallback to Local"
372 );
373 }
374}