Skip to main content

alien_bindings/
provider.rs

1//! Unified BindingsProvider implementation that supports multiple cloud providers
2
3use crate::{
4    error::{binding_env_var, ErrorData, Result},
5    providers::postgres::runtime::PostgresRuntime,
6    traits::{
7        ArtifactRegistry, BindingsProviderApi, Build, Container, Kv, Postgres, Queue,
8        ServiceAccount, Storage, Vault, Worker,
9    },
10};
11
12use crate::credential_source::{MintingCredentialSource, MintingResolver};
13use alien_client_config::ClientConfigExt;
14use alien_core::bindings::PostgresBinding;
15use alien_core::{ClientConfig, Platform, StackState, ENV_OPERATOR_BASE_PLATFORM};
16use alien_error::{AlienError, Context, IntoAlienError};
17use async_trait::async_trait;
18use std::{any::Any, collections::HashMap, sync::Arc};
19use tokio::sync::{OnceCell, RwLock};
20
21/// Direct platform-specific bindings provider.
22/// Routes to appropriate platform implementations based on binding configuration.
23///
24/// Caches loaded bindings by name. Each `load_*` call creates cloud clients
25/// (HTTP connection pools, token caches) which are expensive to initialize. Since
26/// the binding configuration is immutable for the provider's lifetime, the same
27/// binding name always produces the same client — so we cache on first load.
28///
29/// Postgres has different caching semantics because cloud handles contain a resolved
30/// password. Its dedicated runtime owns that policy and its secret-store clients.
31#[derive(Debug, Clone)]
32pub struct BindingsProvider {
33    client_config: ClientConfig,
34    bindings: HashMap<String, serde_json::Value>,
35    /// Per-binding-name cache of loaded binding instances. Keyed by
36    /// `"{trait_name}:{binding_name}"` to avoid collisions across types.
37    /// Each value is a `Box<Arc<dyn Trait>>` erased via `Any`.
38    cache: Arc<RwLock<HashMap<String, Box<dyn Any + Send + Sync>>>>,
39    postgres: Arc<PostgresRuntime>,
40}
41
42/// Environment-backed provider that defers cloud client configuration until the
43/// first binding is actually used.
44///
45/// Runtime-less Containers and Daemons resolve bindings in the application
46/// process. They should still start when no startup secret needs loading, even
47/// if cloud metadata is temporarily unavailable.
48///
49/// On first binding use it resolves credentials in a fixed order (see
50/// [`LazyEnvBindingsProvider::select`]): native/projected `ClientConfig::from_env`
51/// first, then minting-backed resolution if an external/bootstrap caller
52/// explicitly supplied the mint environment contract, otherwise the original
53/// `from_env` error is surfaced unchanged.
54pub struct LazyEnvBindingsProvider {
55    env: HashMap<String, String>,
56    /// Deployment platform parsed at construction on the runtime path
57    /// ([`BindingsProvider::from_env_lazy`] validates it eagerly and `select`
58    /// reuses it instead of re-parsing `env` on first use). `None` on the
59    /// app-facing deferred path ([`BindingsProvider::from_env_deferred`]),
60    /// which must construct with zero environment; `select` then resolves the
61    /// platform on first use of a configured binding.
62    platform: Option<Platform>,
63    /// Binding names present in `env`, parsed at construction. Kept separately
64    /// from the fully-resolved [`BindingsProvider`] so the app-facing kinds can
65    /// answer "is this binding configured?" without first resolving the platform
66    /// or cloud client config. Parsing here also makes malformed binding JSON
67    /// fail fast at construction, and `select` reuses the parse instead of
68    /// re-parsing `env` on first use.
69    bindings: HashMap<String, serde_json::Value>,
70    /// The credential resolution strategy, decided once on first use.
71    resolver: OnceCell<CredentialResolver>,
72}
73
74/// Manual `Debug`: `env` may hold live credential material (cloud secret keys,
75/// the deployment token). Print only the env var *names*, never their values.
76impl std::fmt::Debug for LazyEnvBindingsProvider {
77    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
78        f.debug_struct("LazyEnvBindingsProvider")
79            .field("env_keys", &self.env.keys().collect::<Vec<_>>())
80            .field("resolver", &self.resolver.get())
81            .finish()
82    }
83}
84
85/// How a [`LazyEnvBindingsProvider`] obtains its [`BindingsProvider`], chosen
86/// once at first binding use.
87enum CredentialResolver {
88    /// Native/projected credentials resolved from the environment. The provider
89    /// (and its `ClientConfig`) never changes for the process lifetime.
90    Static(Arc<BindingsProvider>),
91    /// Minting-backed credentials fetched from the manager. The backing provider
92    /// is rebuilt on each re-mint; see [`MintingResolver`]. Boxed so this variant
93    /// doesn't bloat the common `Static` one.
94    Minting(Box<MintingResolver>),
95}
96
97/// Manual `Debug`: the `Static` provider embeds a live `ClientConfig` and the
98/// `Minting` resolver a bearer token / minted config. Never render either.
99impl std::fmt::Debug for CredentialResolver {
100    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
101        match self {
102            CredentialResolver::Static(_) => f.write_str("Static(<redacted>)"),
103            CredentialResolver::Minting(resolver) => {
104                f.debug_tuple("Minting").field(resolver).finish()
105            }
106        }
107    }
108}
109
110impl BindingsProvider {
111    /// Creates a new BindingsProvider with explicit credentials and bindings.
112    ///
113    /// This is the base constructor used by all other convenience constructors.
114    pub fn new(
115        client_config: ClientConfig,
116        bindings: HashMap<String, serde_json::Value>,
117    ) -> Result<Self> {
118        let postgres = Arc::new(PostgresRuntime::new(client_config.clone()));
119        Ok(Self {
120            client_config,
121            bindings,
122            cache: Arc::new(RwLock::new(HashMap::new())),
123            postgres,
124        })
125    }
126
127    /// The workload's resolved cloud credentials (native/projected identity or Alien-minted
128    /// short-lived credentials). Consumers that need to authorize a request themselves — the
129    /// AI gateway signs upstream model calls with this — read it here rather than going
130    /// through a per-resource binding.
131    pub fn client_config(&self) -> &ClientConfig {
132        &self.client_config
133    }
134
135    /// Get a cached binding by type and name, or return None.
136    async fn get_cached<T: Clone + Send + Sync + 'static>(
137        &self,
138        trait_name: &str,
139        binding_name: &str,
140    ) -> Option<T> {
141        let cache_key = format!("{}:{}", trait_name, binding_name);
142        let cache = self.cache.read().await;
143        cache
144            .get(&cache_key)
145            .and_then(|boxed| boxed.downcast_ref::<T>())
146            .cloned()
147    }
148
149    /// Store a binding in the cache.
150    async fn put_cache<T: Clone + Send + Sync + 'static>(
151        &self,
152        trait_name: &str,
153        binding_name: &str,
154        value: T,
155    ) {
156        let cache_key = format!("{}:{}", trait_name, binding_name);
157        let mut cache = self.cache.write().await;
158        cache.insert(cache_key, Box::new(value));
159    }
160
161    /// Creates a BindingsProvider from environment variables (for runtime use).
162    ///
163    /// This parses the platform from ALIEN_DEPLOYMENT_TYPE, loads ClientConfig from environment,
164    /// and extracts all ALIEN_*_BINDING environment variables.
165    pub async fn from_env(env: HashMap<String, String>) -> Result<Self> {
166        // 1. Parse platform from ALIEN_DEPLOYMENT_TYPE
167        let platform = crate::get_platform_from_env(&env)?;
168
169        // 2. Load ClientConfig from environment
170        let client_config = Self::client_config_from_env(platform, &env).await?;
171
172        // 3. Parse all ALIEN_*_BINDING environment variables
173        let bindings = Self::parse_bindings_from_env(&env)?;
174
175        Self::new(client_config, bindings)
176    }
177
178    /// Creates an environment-backed provider without resolving cloud client
179    /// configuration yet.
180    ///
181    /// Startup still validates the platform and binding JSON, but credentials
182    /// are loaded only if application code asks for a binding or runtime-owned
183    /// startup secrets need to be fetched.
184    pub fn from_env_lazy(env: HashMap<String, String>) -> Result<LazyEnvBindingsProvider> {
185        // Runtime path: validate the deployment platform eagerly so a
186        // misconfigured worker fails at startup, not on first binding use.
187        let platform = crate::get_platform_from_env(&env)?;
188        let bindings = Self::parse_bindings_from_env(&env)?;
189
190        Ok(LazyEnvBindingsProvider {
191            env,
192            platform: Some(platform),
193            bindings,
194            resolver: OnceCell::new(),
195        })
196    }
197
198    /// Creates an environment-backed provider that defers *all* platform and
199    /// cloud-client-config resolution to the first use of a *configured*
200    /// binding.
201    ///
202    /// This is the app-facing (napi / [`crate::Bindings`]) path. Its contract:
203    /// constructing with zero environment must succeed, and the first operation
204    /// against a binding that has no `ALIEN_<NAME>_BINDING` must report
205    /// [`ErrorData::BindingNotConfigured`] *before* any platform or credential
206    /// resolution — a missing binding is an application error, not a deployment
207    /// misconfiguration. Binding JSON is still parsed here, so malformed config
208    /// fails fast at construction.
209    ///
210    /// Contrast with [`Self::from_env_lazy`], which eagerly validates the
211    /// deployment platform so long-running runtime processes fail fast at
212    /// startup.
213    pub fn from_env_deferred(env: HashMap<String, String>) -> Result<LazyEnvBindingsProvider> {
214        let bindings = Self::parse_bindings_from_env(&env)?;
215
216        Ok(LazyEnvBindingsProvider {
217            env,
218            // Deferred contract: platform resolution happens on first use of a
219            // configured binding, never at construction.
220            platform: None,
221            bindings,
222            resolver: OnceCell::new(),
223        })
224    }
225
226    async fn client_config_from_env(
227        platform: Platform,
228        env: &HashMap<String, String>,
229    ) -> Result<ClientConfig> {
230        if platform != Platform::Kubernetes {
231            return Self::load_client_config_from_env(platform, env).await;
232        }
233
234        let Some(base_platform) = Self::base_platform_from_env(env)? else {
235            return Self::load_client_config_from_env(platform, env).await;
236        };
237
238        let kubernetes = match Self::load_client_config_from_env(Platform::Kubernetes, env).await? {
239            ClientConfig::Kubernetes(kubernetes) => kubernetes,
240            _ => unreachable!("kubernetes platform must produce a Kubernetes client config"),
241        };
242        let cloud = Self::load_client_config_from_env(base_platform, env).await?;
243
244        Ok(ClientConfig::KubernetesCloud {
245            kubernetes,
246            cloud: Box::new(cloud),
247        })
248    }
249
250    fn base_platform_from_env(env: &HashMap<String, String>) -> Result<Option<Platform>> {
251        let Some(base_platform) = env.get(ENV_OPERATOR_BASE_PLATFORM) else {
252            return Ok(None);
253        };
254
255        let parsed: Platform = base_platform.parse().map_err(|reason| {
256            AlienError::new(ErrorData::InvalidEnvironmentVariable {
257                variable_name: ENV_OPERATOR_BASE_PLATFORM.to_string(),
258                value: base_platform.clone(),
259                reason,
260            })
261        })?;
262
263        if !matches!(parsed, Platform::Aws | Platform::Gcp | Platform::Azure) {
264            return Err(AlienError::new(ErrorData::InvalidEnvironmentVariable {
265                variable_name: ENV_OPERATOR_BASE_PLATFORM.to_string(),
266                value: base_platform.clone(),
267                reason: "Kubernetes base platform must be aws, gcp, or azure".to_string(),
268            }));
269        }
270
271        Ok(Some(parsed))
272    }
273
274    async fn load_client_config_from_env(
275        platform: Platform,
276        env: &HashMap<String, String>,
277    ) -> Result<ClientConfig> {
278        ClientConfig::from_env(platform, env).await.map_err(|e| {
279            AlienError::new(ErrorData::ClientConfigInvalid {
280                platform,
281                message: format!("Failed to load client config: {}", e),
282            })
283        })
284    }
285
286    /// Parses all ALIEN_*_BINDING environment variables into a map.
287    fn parse_bindings_from_env(
288        env: &HashMap<String, String>,
289    ) -> Result<HashMap<String, serde_json::Value>> {
290        let mut bindings = HashMap::new();
291        for (key, value) in env {
292            if key.starts_with("ALIEN_") && key.ends_with("_BINDING") {
293                let binding_name = key
294                    .strip_prefix("ALIEN_")
295                    .unwrap()
296                    .strip_suffix("_BINDING")
297                    .unwrap()
298                    .to_lowercase()
299                    .replace('_', "-");
300                let parsed: serde_json::Value = serde_json::from_str(value)
301                    .into_alien_error()
302                    .context(ErrorData::BindingConfigInvalid {
303                        env_var: key.clone(),
304                        binding_name: binding_name.clone(),
305                        reason: "Failed to parse binding JSON".to_string(),
306                    })?;
307                bindings.insert(binding_name, parsed);
308            }
309        }
310        Ok(bindings)
311    }
312
313    /// Look up a binding's JSON and parse it into its strongly-typed form.
314    ///
315    /// Maps a missing binding to [`ErrorData::not_configured`] and a malformed
316    /// one to [`ErrorData::config_invalid`], tagged with `type_label` (e.g.
317    /// `"storage"`, `"KV"`) so the message reads `Failed to parse <label> binding`.
318    fn parse_binding<T: serde::de::DeserializeOwned>(
319        &self,
320        binding_name: &str,
321        type_label: &str,
322    ) -> Result<T> {
323        let binding_json = self
324            .bindings
325            .get(binding_name)
326            .ok_or_else(|| AlienError::new(ErrorData::not_configured(binding_name)))?;
327        serde_json::from_value(binding_json.clone())
328            .into_alien_error()
329            .context(ErrorData::config_invalid(
330                binding_name,
331                format!("Failed to parse {type_label} binding"),
332            ))
333    }
334
335    /// Pattern 1: Creates a BindingsProvider from stack state and client config.
336    ///
337    /// Convenience helper that extracts bindings from stack state's remote_binding_params.
338    /// Only resources with `remote_access: true` will have binding params available.
339    ///
340    /// **When to use:** You already have `ClientConfig` and `StackState`.
341    ///
342    /// **Example use cases:**
343    /// - alien-deployment (has credentials from RemoteAccessResolver)
344    /// - Platform API backend (has stack state from DB)
345    pub fn from_stack_state(stack_state: &StackState, client_config: ClientConfig) -> Result<Self> {
346        let bindings = stack_state
347            .resources
348            .iter()
349            .filter_map(|(id, state)| {
350                state
351                    .remote_binding_params
352                    .as_ref()
353                    .map(|p| (id.clone(), p.clone()))
354            })
355            .collect();
356
357        Self::new(client_config, bindings)
358    }
359}
360
361impl LazyEnvBindingsProvider {
362    /// Resolve (once) which credential strategy to use, then return a provider
363    /// backed by fresh-enough credentials.
364    ///
365    /// The strategy selection happens exactly once (via `OnceCell`); on the
366    /// minting path each call additionally checks credential freshness and
367    /// re-mints on access if stale.
368    pub async fn provider(&self) -> Result<Arc<BindingsProvider>> {
369        let resolver = self
370            .resolver
371            .get_or_try_init(|| async { self.select().await })
372            .await?;
373
374        match resolver {
375            CredentialResolver::Static(provider) => Ok(provider.clone()),
376            CredentialResolver::Minting(minting) => minting.provider().await,
377        }
378    }
379
380    /// Decide the credential strategy at first use, in a fixed order:
381    ///
382    /// 1. Native/projected `ClientConfig::from_env` succeeds → use it, never mint.
383    /// 2. Else if the complete external/bootstrap mint env contract is present
384    ///    → mint.
385    /// 3. Else → surface the original `from_env` error unchanged.
386    async fn select(&self) -> Result<CredentialResolver> {
387        // Binding JSON (and, on the `from_env_lazy` path, the platform) was
388        // already parsed and validated at construction; `env` cannot have
389        // changed since, so re-parsing would just repeat that work for the
390        // same result. On the `from_env_deferred` path the platform was
391        // deliberately not resolved at construction, so resolve it now.
392        let platform = match self.platform {
393            Some(platform) => platform,
394            None => crate::get_platform_from_env(&self.env)?,
395        };
396        match BindingsProvider::client_config_from_env(platform, &self.env).await {
397            Ok(client_config) => Ok(CredentialResolver::Static(Arc::new(BindingsProvider::new(
398                client_config,
399                self.bindings.clone(),
400            )?))),
401            Err(from_env_error) => match MintingCredentialSource::from_env(&self.env)? {
402                Some(source) => Ok(CredentialResolver::Minting(Box::new(MintingResolver::new(
403                    source,
404                    self.bindings.clone(),
405                )))),
406                // No mint contract: the environment simply couldn't produce
407                // credentials. Preserve the exact original error.
408                None => Err(from_env_error),
409            },
410        }
411    }
412
413    /// Fail fast with [`ErrorData::BindingNotConfigured`] when `binding_name`
414    /// has no `ALIEN_<NAME>_BINDING` entry, *before* any platform / cloud
415    /// client-config resolution. This is what lets a zero-env app construct
416    /// bindings and get a clean BINDING_NOT_CONFIGURED (not a deployment error)
417    /// on the first op against a missing binding. A binding that *is* present
418    /// falls through to normal resolution, so an existing binding on a cloud
419    /// platform without credentials still surfaces the client-config error.
420    ///
421    /// Called at the entry of every `load_*` method below, app-facing or not:
422    /// `parse_binding` (in [`BindingsProvider`]) would return the identical
423    /// `not_configured` error for a missing name once resolution reaches it,
424    /// so this guard only *reorders* that error ahead of platform/credential
425    /// resolution — it never changes which bindings succeed or fail. Keeping
426    /// it uniform means a newly added `load_*` method can't silently regress
427    /// the zero-env contract by omission.
428    fn ensure_binding_present(&self, binding_name: &str) -> Result<()> {
429        if self.bindings.contains_key(binding_name) {
430            Ok(())
431        } else {
432            Err(AlienError::new(ErrorData::not_configured(binding_name)))
433        }
434    }
435}
436
437#[async_trait]
438impl BindingsProviderApi for LazyEnvBindingsProvider {
439    async fn load_storage(&self, binding_name: &str) -> Result<Arc<dyn Storage>> {
440        self.ensure_binding_present(binding_name)?;
441        self.provider().await?.load_storage(binding_name).await
442    }
443
444    async fn load_build(&self, binding_name: &str) -> Result<Arc<dyn Build>> {
445        self.ensure_binding_present(binding_name)?;
446        self.provider().await?.load_build(binding_name).await
447    }
448
449    async fn load_artifact_registry(
450        &self,
451        binding_name: &str,
452    ) -> Result<Arc<dyn ArtifactRegistry>> {
453        self.ensure_binding_present(binding_name)?;
454        self.provider()
455            .await?
456            .load_artifact_registry(binding_name)
457            .await
458    }
459
460    async fn load_vault(&self, binding_name: &str) -> Result<Arc<dyn Vault>> {
461        self.ensure_binding_present(binding_name)?;
462        self.provider().await?.load_vault(binding_name).await
463    }
464
465    async fn load_kv(&self, binding_name: &str) -> Result<Arc<dyn Kv>> {
466        self.ensure_binding_present(binding_name)?;
467        self.provider().await?.load_kv(binding_name).await
468    }
469
470    async fn load_postgres(&self, binding_name: &str) -> Result<Arc<dyn Postgres>> {
471        self.ensure_binding_present(binding_name)?;
472        self.provider().await?.load_postgres(binding_name).await
473    }
474
475    async fn load_queue(&self, binding_name: &str) -> Result<Arc<dyn Queue>> {
476        self.ensure_binding_present(binding_name)?;
477        self.provider().await?.load_queue(binding_name).await
478    }
479
480    async fn load_worker(&self, binding_name: &str) -> Result<Arc<dyn Worker>> {
481        self.ensure_binding_present(binding_name)?;
482        self.provider().await?.load_worker(binding_name).await
483    }
484
485    async fn load_container(&self, binding_name: &str) -> Result<Arc<dyn Container>> {
486        self.ensure_binding_present(binding_name)?;
487        self.provider().await?.load_container(binding_name).await
488    }
489
490    async fn load_service_account(&self, binding_name: &str) -> Result<Arc<dyn ServiceAccount>> {
491        self.ensure_binding_present(binding_name)?;
492        self.provider()
493            .await?
494            .load_service_account(binding_name)
495            .await
496    }
497}
498
499#[async_trait]
500impl BindingsProviderApi for BindingsProvider {
501    async fn load_storage(&self, binding_name: &str) -> Result<Arc<dyn Storage>> {
502        if let Some(cached) = self
503            .get_cached::<Arc<dyn Storage>>("storage", binding_name)
504            .await
505        {
506            return Ok(cached);
507        }
508
509        use alien_core::bindings::StorageBinding;
510
511        // Get binding JSON from our pre-parsed map
512        let binding: StorageBinding = self.parse_binding(binding_name, "storage")?;
513
514        let result: Arc<dyn Storage> = match binding {
515            #[cfg(feature = "aws")]
516            StorageBinding::S3(config) => {
517                use crate::providers::storage::aws_s3::S3Storage;
518
519                // Get AWS config from our stored ClientConfig
520                let aws_config = self.client_config.aws_config().ok_or_else(|| {
521                    AlienError::new(ErrorData::ClientConfigInvalid {
522                        platform: Platform::Aws,
523                        message: "AWS config not available".to_string(),
524                    })
525                })?;
526
527                let credentials =
528                    alien_aws_clients::AwsCredentialProvider::from_config(aws_config.clone())
529                        .await
530                        .context(ErrorData::BindingSetupFailed {
531                            binding_type: "AWS S3 storage".to_string(),
532                            reason: "Failed to create credential provider".to_string(),
533                        })?;
534
535                // Extract bucket name from binding
536                let bucket_name = config
537                    .bucket_name
538                    .into_value(binding_name, "bucket_name")
539                    .context(ErrorData::config_invalid(
540                        binding_name,
541                        "Failed to extract bucket_name from S3 binding",
542                    ))?;
543
544                let storage: Arc<dyn Storage> = Arc::new(S3Storage::new(bucket_name, credentials)?);
545                Ok(storage)
546            }
547            #[cfg(not(feature = "aws"))]
548            StorageBinding::S3 { .. } => Err(AlienError::new(ErrorData::FeatureNotEnabled {
549                feature: "aws".to_string(),
550            })),
551
552            #[cfg(feature = "azure")]
553            StorageBinding::Blob(config) => {
554                use crate::providers::storage::azure_blob::BlobStorage;
555
556                let azure_config = self.client_config.azure_config().ok_or_else(|| {
557                    AlienError::new(ErrorData::ClientConfigInvalid {
558                        platform: Platform::Azure,
559                        message: "Azure config not available".to_string(),
560                    })
561                })?;
562
563                // Extract container and account names from binding
564                let container_name = config
565                    .container_name
566                    .into_value(binding_name, "container_name")
567                    .context(ErrorData::config_invalid(
568                        binding_name,
569                        "Failed to extract container_name from Blob binding",
570                    ))?;
571
572                let account_name = config
573                    .account_name
574                    .into_value(binding_name, "account_name")
575                    .context(ErrorData::config_invalid(
576                        binding_name,
577                        "Failed to extract account_name from Blob binding",
578                    ))?;
579
580                let storage: Arc<dyn Storage> = Arc::new(BlobStorage::new(
581                    container_name,
582                    account_name,
583                    azure_config,
584                )?);
585                Ok(storage)
586            }
587            #[cfg(not(feature = "azure"))]
588            StorageBinding::Blob { .. } => Err(AlienError::new(ErrorData::FeatureNotEnabled {
589                feature: "azure".to_string(),
590            })),
591
592            #[cfg(feature = "gcp")]
593            StorageBinding::Gcs(config) => {
594                use crate::providers::storage::gcp_gcs::GcsStorage;
595
596                let gcp_config = self.client_config.gcp_config().ok_or_else(|| {
597                    AlienError::new(ErrorData::ClientConfigInvalid {
598                        platform: Platform::Gcp,
599                        message: "GCP config not available".to_string(),
600                    })
601                })?;
602
603                // Extract bucket name from binding
604                let bucket_name = config
605                    .bucket_name
606                    .into_value(binding_name, "bucket_name")
607                    .context(ErrorData::config_invalid(
608                        binding_name,
609                        "Failed to extract bucket_name from Gcs binding",
610                    ))?;
611
612                let storage: Arc<dyn Storage> = Arc::new(GcsStorage::new(bucket_name, gcp_config)?);
613                Ok(storage)
614            }
615            #[cfg(not(feature = "gcp"))]
616            StorageBinding::Gcs { .. } => Err(AlienError::new(ErrorData::FeatureNotEnabled {
617                feature: "gcp".to_string(),
618            })),
619
620            #[cfg(feature = "local")]
621            StorageBinding::Local(config) => {
622                use crate::providers::storage::local::LocalStorage;
623
624                // Extract storage path from binding
625                let storage_path = config
626                    .storage_path
627                    .into_value(binding_name, "storage_path")
628                    .context(ErrorData::config_invalid(
629                        binding_name,
630                        "Failed to extract storage_path from Local binding",
631                    ))?;
632
633                let storage: Arc<dyn Storage> = Arc::new(LocalStorage::new(storage_path)?);
634                Ok(storage)
635            }
636            #[cfg(not(feature = "local"))]
637            StorageBinding::Local { .. } => Err(AlienError::new(ErrorData::FeatureNotEnabled {
638                feature: "local".to_string(),
639            })),
640        }?;
641
642        self.put_cache("storage", binding_name, result.clone())
643            .await;
644        Ok(result)
645    }
646
647    async fn load_build(&self, binding_name: &str) -> Result<Arc<dyn Build>> {
648        use alien_core::bindings::BuildBinding;
649
650        let binding: BuildBinding = self.parse_binding(binding_name, "build")?;
651
652        match binding {
653            #[cfg(feature = "aws")]
654            BuildBinding::Codebuild { .. } => {
655                use crate::providers::build::codebuild::CodebuildBuild;
656
657                let aws_config = self.client_config.aws_config().ok_or_else(|| {
658                    AlienError::new(ErrorData::ClientConfigInvalid {
659                        platform: Platform::Aws,
660                        message: "AWS config not available".to_string(),
661                    })
662                })?;
663                let credentials =
664                    alien_aws_clients::AwsCredentialProvider::from_config(aws_config.clone())
665                        .await
666                        .context(ErrorData::ClientConfigInvalid {
667                            platform: Platform::Aws,
668                            message: "Failed to create AWS credential provider".to_string(),
669                        })?;
670
671                let build = Arc::new(
672                    CodebuildBuild::new(binding_name.to_string(), binding, &credentials)
673                        .await
674                        .context(ErrorData::config_invalid(
675                            binding_name,
676                            "Failed to initialize AWS CodeBuild client",
677                        ))?,
678                );
679                Ok(build)
680            }
681            #[cfg(not(feature = "aws"))]
682            BuildBinding::Codebuild { .. } => Err(AlienError::new(ErrorData::FeatureNotEnabled {
683                feature: "aws".to_string(),
684            })),
685
686            #[cfg(feature = "azure")]
687            BuildBinding::Aca { .. } => {
688                use crate::providers::build::aca::AcaBuild;
689
690                let azure_config = self.client_config.azure_config().ok_or_else(|| {
691                    AlienError::new(ErrorData::ClientConfigInvalid {
692                        platform: Platform::Azure,
693                        message: "Azure config not available".to_string(),
694                    })
695                })?;
696
697                let build = Arc::new(
698                    AcaBuild::new(binding_name.to_string(), binding, azure_config)
699                        .await
700                        .context(ErrorData::config_invalid(
701                            binding_name,
702                            "Failed to initialize Azure Container Apps build",
703                        ))?,
704                );
705                Ok(build)
706            }
707            #[cfg(not(feature = "azure"))]
708            BuildBinding::Aca { .. } => Err(AlienError::new(ErrorData::FeatureNotEnabled {
709                feature: "azure".to_string(),
710            })),
711
712            #[cfg(feature = "gcp")]
713            BuildBinding::Cloudbuild { .. } => {
714                use crate::providers::build::cloudbuild::CloudbuildBuild;
715
716                let gcp_config = self.client_config.gcp_config().ok_or_else(|| {
717                    AlienError::new(ErrorData::ClientConfigInvalid {
718                        platform: Platform::Gcp,
719                        message: "GCP config not available".to_string(),
720                    })
721                })?;
722
723                let build = Arc::new(
724                    CloudbuildBuild::new(binding_name.to_string(), binding, gcp_config)
725                        .await
726                        .context(ErrorData::config_invalid(
727                            binding_name,
728                            "Failed to initialize GCP Cloud Build client",
729                        ))?,
730                );
731                Ok(build)
732            }
733            #[cfg(not(feature = "gcp"))]
734            BuildBinding::Cloudbuild { .. } => Err(AlienError::new(ErrorData::FeatureNotEnabled {
735                feature: "gcp".to_string(),
736            })),
737
738            #[cfg(feature = "local")]
739            BuildBinding::Local { .. } => {
740                use crate::providers::build::local::LocalBuild;
741
742                let build = Arc::new(LocalBuild::new(binding_name.to_string(), binding)?);
743                Ok(build)
744            }
745            #[cfg(not(feature = "local"))]
746            BuildBinding::Local { .. } => Err(AlienError::new(ErrorData::FeatureNotEnabled {
747                feature: "local".to_string(),
748            })),
749
750            #[cfg(feature = "kubernetes")]
751            BuildBinding::Kubernetes { .. } => {
752                use crate::providers::build::kubernetes::KubernetesBuild;
753
754                let build =
755                    Arc::new(KubernetesBuild::new(binding_name.to_string(), binding).await?);
756                Ok(build)
757            }
758            #[cfg(not(feature = "kubernetes"))]
759            BuildBinding::Kubernetes { .. } => Err(AlienError::new(ErrorData::FeatureNotEnabled {
760                feature: "kubernetes".to_string(),
761            })),
762        }
763    }
764
765    async fn load_artifact_registry(
766        &self,
767        binding_name: &str,
768    ) -> Result<Arc<dyn ArtifactRegistry>> {
769        if let Some(cached) = self
770            .get_cached::<Arc<dyn ArtifactRegistry>>("artifact_registry", binding_name)
771            .await
772        {
773            return Ok(cached);
774        }
775
776        use alien_core::bindings::ArtifactRegistryBinding;
777
778        let binding: ArtifactRegistryBinding =
779            self.parse_binding(binding_name, "artifact registry")?;
780
781        let registry: Arc<dyn ArtifactRegistry> = match binding {
782            #[cfg(feature = "aws")]
783            ArtifactRegistryBinding::Ecr { .. } => {
784                use crate::providers::artifact_registry::ecr::EcrArtifactRegistry;
785
786                let aws_config = self.client_config.aws_config().ok_or_else(|| {
787                    AlienError::new(ErrorData::ClientConfigInvalid {
788                        platform: Platform::Aws,
789                        message: "AWS config not available".to_string(),
790                    })
791                })?;
792                let credentials =
793                    alien_aws_clients::AwsCredentialProvider::from_config(aws_config.clone())
794                        .await
795                        .context(ErrorData::ClientConfigInvalid {
796                            platform: Platform::Aws,
797                            message: "Failed to create AWS credential provider".to_string(),
798                        })?;
799
800                let registry: Arc<dyn ArtifactRegistry> = Arc::new(
801                    EcrArtifactRegistry::new(binding_name.to_string(), binding, &credentials)
802                        .await
803                        .context(ErrorData::config_invalid(
804                            binding_name,
805                            "Failed to initialize AWS ECR artifact registry",
806                        ))?,
807                );
808                Ok(registry)
809            }
810            #[cfg(not(feature = "aws"))]
811            ArtifactRegistryBinding::Ecr { .. } => {
812                Err(AlienError::new(ErrorData::FeatureNotEnabled {
813                    feature: "aws".to_string(),
814                }))
815            }
816
817            #[cfg(feature = "azure")]
818            ArtifactRegistryBinding::Acr { .. } => {
819                use crate::providers::artifact_registry::acr::AcrArtifactRegistry;
820
821                let azure_config = self.client_config.azure_config().ok_or_else(|| {
822                    AlienError::new(ErrorData::ClientConfigInvalid {
823                        platform: Platform::Azure,
824                        message: "Azure config not available".to_string(),
825                    })
826                })?;
827
828                let registry: Arc<dyn ArtifactRegistry> = Arc::new(
829                    AcrArtifactRegistry::new(binding_name.to_string(), binding, azure_config)
830                        .await
831                        .context(ErrorData::config_invalid(
832                            binding_name,
833                            "Failed to initialize Azure ACR artifact registry",
834                        ))?,
835                );
836                Ok(registry)
837            }
838            #[cfg(not(feature = "azure"))]
839            ArtifactRegistryBinding::Acr { .. } => {
840                Err(AlienError::new(ErrorData::FeatureNotEnabled {
841                    feature: "azure".to_string(),
842                }))
843            }
844
845            #[cfg(feature = "gcp")]
846            ArtifactRegistryBinding::Gar { .. } => {
847                use crate::providers::artifact_registry::gar::GarArtifactRegistry;
848
849                let gcp_config = self.client_config.gcp_config().ok_or_else(|| {
850                    AlienError::new(ErrorData::ClientConfigInvalid {
851                        platform: Platform::Gcp,
852                        message: "GCP config not available".to_string(),
853                    })
854                })?;
855
856                let registry: Arc<dyn ArtifactRegistry> = Arc::new(
857                    GarArtifactRegistry::new(binding_name.to_string(), binding, gcp_config)
858                        .await
859                        .context(ErrorData::config_invalid(
860                            binding_name,
861                            "Failed to initialize GCP GAR artifact registry",
862                        ))?,
863                );
864                Ok(registry)
865            }
866            #[cfg(not(feature = "gcp"))]
867            ArtifactRegistryBinding::Gar { .. } => {
868                Err(AlienError::new(ErrorData::FeatureNotEnabled {
869                    feature: "gcp".to_string(),
870                }))
871            }
872
873            #[cfg(feature = "local")]
874            ArtifactRegistryBinding::Local { .. } => {
875                use crate::providers::artifact_registry::local::LocalArtifactRegistry;
876
877                let registry: Arc<dyn ArtifactRegistry> = Arc::new(
878                    LocalArtifactRegistry::new(binding_name.to_string(), binding.clone()).await?,
879                );
880                Ok(registry)
881            }
882            #[cfg(not(feature = "local"))]
883            ArtifactRegistryBinding::Local { .. } => {
884                Err(AlienError::new(ErrorData::FeatureNotEnabled {
885                    feature: "local".to_string(),
886                }))
887            }
888        }?;
889
890        self.put_cache("artifact_registry", binding_name, registry.clone())
891            .await;
892        Ok(registry)
893    }
894
895    async fn load_vault(&self, binding_name: &str) -> Result<Arc<dyn Vault>> {
896        if let Some(cached) = self
897            .get_cached::<Arc<dyn Vault>>("vault", binding_name)
898            .await
899        {
900            return Ok(cached);
901        }
902
903        use alien_core::bindings::VaultBinding;
904
905        let binding: VaultBinding = self.parse_binding(binding_name, "vault")?;
906
907        let result: Arc<dyn Vault> = match binding {
908            #[cfg(feature = "aws")]
909            VaultBinding::ParameterStore(config) => {
910                use crate::providers::vault::aws_parameter_store::AwsParameterStoreVault;
911                use alien_aws_clients::ssm::SsmClient;
912
913                let aws_config = self.client_config.aws_config().ok_or_else(|| {
914                    AlienError::new(ErrorData::ClientConfigInvalid {
915                        platform: Platform::Aws,
916                        message: "AWS config not available".to_string(),
917                    })
918                })?;
919                let credentials =
920                    alien_aws_clients::AwsCredentialProvider::from_config(aws_config.clone())
921                        .await
922                        .context(ErrorData::ClientConfigInvalid {
923                            platform: Platform::Aws,
924                            message: "Failed to create AWS credential provider".to_string(),
925                        })?;
926
927                let client = Arc::new(SsmClient::new(
928                    crate::http_client::create_http_client(),
929                    credentials,
930                ));
931
932                // Extract the vault prefix from the binding configuration
933                let vault_prefix = config
934                    .vault_prefix
935                    .into_value(&binding_name, "vault_prefix")
936                    .context(ErrorData::config_invalid(
937                        binding_name,
938                        "Failed to extract vault_prefix from ParameterStore binding",
939                    ))?;
940
941                let vault: Arc<dyn Vault> =
942                    Arc::new(AwsParameterStoreVault::new(client, vault_prefix));
943                Ok(vault)
944            }
945            #[cfg(not(feature = "aws"))]
946            VaultBinding::ParameterStore(_) => Err(AlienError::new(ErrorData::FeatureNotEnabled {
947                feature: "aws".to_string(),
948            })),
949
950            #[cfg(feature = "azure")]
951            VaultBinding::KeyVault(config) => {
952                use crate::providers::vault::azure_key_vault::AzureKeyVault;
953                use alien_azure_clients::keyvault::AzureKeyVaultSecretsClient;
954                use alien_azure_clients::AzureTokenCache;
955
956                let azure_config = self.client_config.azure_config().ok_or_else(|| {
957                    AlienError::new(ErrorData::ClientConfigInvalid {
958                        platform: Platform::Azure,
959                        message: "Azure config not available".to_string(),
960                    })
961                })?;
962
963                let client = Arc::new(AzureKeyVaultSecretsClient::new(
964                    crate::http_client::create_http_client(),
965                    AzureTokenCache::new(azure_config.clone()),
966                ));
967
968                // Extract the vault name from the binding configuration
969                let vault_name = config
970                    .vault_name
971                    .into_value(&binding_name, "vault_name")
972                    .context(ErrorData::config_invalid(
973                        binding_name,
974                        "Failed to extract vault_name from KeyVault binding",
975                    ))?;
976
977                // Construct the vault base URL
978                // Azure Key Vault URLs typically follow: https://{vault-name}.vault.azure.net/
979                let vault_base_url = format!("https://{}.vault.azure.net", vault_name);
980
981                let vault: Arc<dyn Vault> = Arc::new(AzureKeyVault::new(client, vault_base_url));
982                Ok(vault)
983            }
984            #[cfg(not(feature = "azure"))]
985            VaultBinding::KeyVault(_) => Err(AlienError::new(ErrorData::FeatureNotEnabled {
986                feature: "azure".to_string(),
987            })),
988
989            #[cfg(feature = "gcp")]
990            VaultBinding::SecretManager(config) => {
991                use crate::providers::vault::gcp_secret_manager::GcpSecretManagerVault;
992                use alien_gcp_clients::secret_manager::SecretManagerClient;
993
994                let gcp_config = self.client_config.gcp_config().ok_or_else(|| {
995                    AlienError::new(ErrorData::ClientConfigInvalid {
996                        platform: Platform::Gcp,
997                        message: "GCP config not available".to_string(),
998                    })
999                })?;
1000
1001                let client = Arc::new(SecretManagerClient::new(
1002                    crate::http_client::create_http_client(),
1003                    gcp_config.clone(),
1004                ));
1005
1006                // Extract the vault prefix from the binding configuration
1007                let vault_prefix = config
1008                    .vault_prefix
1009                    .into_value(&binding_name, "vault_prefix")
1010                    .context(ErrorData::config_invalid(
1011                        binding_name,
1012                        "Failed to extract vault_prefix from SecretManager binding",
1013                    ))?;
1014
1015                let vault: Arc<dyn Vault> = Arc::new(GcpSecretManagerVault::new(
1016                    client,
1017                    vault_prefix,
1018                    gcp_config.project_id.clone(),
1019                ));
1020                Ok(vault)
1021            }
1022            #[cfg(not(feature = "gcp"))]
1023            VaultBinding::SecretManager(_) => Err(AlienError::new(ErrorData::FeatureNotEnabled {
1024                feature: "gcp".to_string(),
1025            })),
1026
1027            #[cfg(feature = "local")]
1028            VaultBinding::Local(config) => {
1029                use crate::providers::vault::local::LocalVault;
1030
1031                let vault_dir = config
1032                    .data_dir
1033                    .into_value(binding_name, "data_dir")
1034                    .context(ErrorData::config_invalid(
1035                        binding_name,
1036                        "Failed to extract data_dir from vault binding",
1037                    ))?;
1038
1039                let vault: Arc<dyn Vault> = Arc::new(LocalVault::new(
1040                    binding_name.to_string(),
1041                    std::path::PathBuf::from(vault_dir),
1042                ));
1043                Ok(vault)
1044            }
1045            #[cfg(not(feature = "local"))]
1046            VaultBinding::Local { .. } => Err(AlienError::new(ErrorData::FeatureNotEnabled {
1047                feature: "local".to_string(),
1048            })),
1049
1050            #[cfg(feature = "kubernetes")]
1051            VaultBinding::KubernetesSecret(config) => {
1052                use crate::providers::vault::kubernetes_secret::KubernetesSecretVault;
1053                use alien_k8s_clients::{secrets::SecretsApi, KubernetesClient};
1054
1055                let kubernetes_config =
1056                    self.client_config.kubernetes_config().ok_or_else(|| {
1057                        AlienError::new(ErrorData::ClientConfigInvalid {
1058                            platform: Platform::Kubernetes,
1059                            message: "Kubernetes config not available".to_string(),
1060                        })
1061                    })?;
1062
1063                let kubernetes_client = KubernetesClient::new(kubernetes_config.clone())
1064                    .await
1065                    .context(ErrorData::CloudPlatformError {
1066                        message: "Failed to create Kubernetes client for vault".to_string(),
1067                        resource_id: None,
1068                    })?;
1069
1070                let client: Arc<dyn SecretsApi> = Arc::new(kubernetes_client);
1071
1072                // Extract namespace and vault prefix from binding
1073                let namespace = config
1074                    .namespace
1075                    .into_value(binding_name, "namespace")
1076                    .context(ErrorData::config_invalid(
1077                        binding_name,
1078                        "Failed to extract namespace from KubernetesSecret binding",
1079                    ))?;
1080
1081                let vault_prefix = config
1082                    .vault_prefix
1083                    .into_value(binding_name, "vault_prefix")
1084                    .context(ErrorData::config_invalid(
1085                        binding_name,
1086                        "Failed to extract vault_prefix from KubernetesSecret binding",
1087                    ))?;
1088
1089                let vault: Arc<dyn Vault> =
1090                    Arc::new(KubernetesSecretVault::new(client, namespace, vault_prefix));
1091                Ok(vault)
1092            }
1093            #[cfg(not(feature = "kubernetes"))]
1094            VaultBinding::KubernetesSecret(_) => {
1095                Err(AlienError::new(ErrorData::FeatureNotEnabled {
1096                    feature: "kubernetes".to_string(),
1097                }))
1098            }
1099        }?;
1100
1101        self.put_cache("vault", binding_name, result.clone()).await;
1102        Ok(result)
1103    }
1104
1105    async fn load_kv(&self, binding_name: &str) -> Result<Arc<dyn Kv>> {
1106        if let Some(cached) = self.get_cached::<Arc<dyn Kv>>("kv", binding_name).await {
1107            return Ok(cached);
1108        }
1109
1110        use alien_core::bindings::KvBinding;
1111
1112        let binding: KvBinding = self.parse_binding(binding_name, "KV")?;
1113
1114        let result: Arc<dyn Kv> = match binding {
1115            #[cfg(feature = "aws")]
1116            KvBinding::Dynamodb(config) => {
1117                use crate::providers::kv::aws_dynamodb::AwsDynamodbKv;
1118
1119                let table_name = config
1120                    .table_name
1121                    .into_value(binding_name, "table_name")
1122                    .context(ErrorData::config_invalid(
1123                        binding_name,
1124                        "Failed to extract table_name from DynamoDB binding",
1125                    ))?;
1126
1127                let aws_config = self.client_config.aws_config().ok_or_else(|| {
1128                    AlienError::new(ErrorData::ClientConfigInvalid {
1129                        platform: Platform::Aws,
1130                        message: "AWS config not available".to_string(),
1131                    })
1132                })?;
1133
1134                let credentials =
1135                    alien_aws_clients::AwsCredentialProvider::from_config(aws_config.clone())
1136                        .await
1137                        .context(ErrorData::ClientConfigInvalid {
1138                            platform: Platform::Aws,
1139                            message: "Failed to create AWS credential provider".to_string(),
1140                        })?;
1141                let dynamodb_client = alien_aws_clients::dynamodb::DynamoDbClient::new(
1142                    crate::http_client::create_http_client(),
1143                    credentials,
1144                );
1145                let kv_impl = AwsDynamodbKv::new(table_name, dynamodb_client);
1146                let kv: Arc<dyn Kv> = Arc::new(kv_impl);
1147                Ok(kv)
1148            }
1149            #[cfg(not(feature = "aws"))]
1150            KvBinding::Dynamodb(_) => Err(AlienError::new(ErrorData::FeatureNotEnabled {
1151                feature: "aws".to_string(),
1152            })),
1153
1154            #[cfg(feature = "gcp")]
1155            KvBinding::Firestore(config) => {
1156                use crate::providers::kv::gcp_firestore::GcpFirestoreKv;
1157                use alien_gcp_clients::firestore::FirestoreClient;
1158
1159                let gcp_config = self.client_config.gcp_config().ok_or_else(|| {
1160                    AlienError::new(ErrorData::ClientConfigInvalid {
1161                        platform: Platform::Gcp,
1162                        message: "GCP config not available".to_string(),
1163                    })
1164                })?;
1165
1166                let client = FirestoreClient::new(
1167                    crate::http_client::create_http_client(),
1168                    gcp_config.clone(),
1169                );
1170
1171                let project_id = config
1172                    .project_id
1173                    .into_value(binding_name, "project_id")
1174                    .context(ErrorData::config_invalid(
1175                        binding_name,
1176                        "Failed to extract project_id from Firestore binding",
1177                    ))?;
1178
1179                let database_id = config
1180                    .database_id
1181                    .into_value(binding_name, "database_id")
1182                    .context(ErrorData::config_invalid(
1183                        binding_name,
1184                        "Failed to extract database_id from Firestore binding",
1185                    ))?;
1186
1187                let collection_name = config
1188                    .collection_name
1189                    .into_value(binding_name, "collection_name")
1190                    .context(ErrorData::config_invalid(
1191                        binding_name,
1192                        "Failed to extract collection_name from Firestore binding",
1193                    ))?;
1194
1195                let kv: Arc<dyn Kv> = Arc::new(GcpFirestoreKv::new(
1196                    client,
1197                    project_id,
1198                    database_id,
1199                    collection_name,
1200                )?);
1201                Ok(kv)
1202            }
1203            #[cfg(not(feature = "gcp"))]
1204            KvBinding::Firestore(_) => Err(AlienError::new(ErrorData::FeatureNotEnabled {
1205                feature: "gcp".to_string(),
1206            })),
1207
1208            #[cfg(feature = "azure")]
1209            KvBinding::TableStorage(config) => {
1210                use crate::providers::kv::azure_table_storage::AzureTableStorageKv;
1211                use alien_azure_clients::tables::AzureTableStorageClient;
1212                use alien_azure_clients::AzureTokenCache;
1213
1214                let azure_config = self.client_config.azure_config().ok_or_else(|| {
1215                    AlienError::new(ErrorData::ClientConfigInvalid {
1216                        platform: Platform::Azure,
1217                        message: "Azure config not available".to_string(),
1218                    })
1219                })?;
1220
1221                let resource_group_name = config
1222                    .resource_group_name
1223                    .into_value(binding_name, "resource_group_name")
1224                    .context(ErrorData::config_invalid(
1225                        binding_name,
1226                        "Failed to extract resource_group_name from TableStorage binding",
1227                    ))?;
1228
1229                let account_name = config
1230                    .account_name
1231                    .into_value(binding_name, "account_name")
1232                    .context(ErrorData::config_invalid(
1233                        binding_name,
1234                        "Failed to extract account_name from TableStorage binding",
1235                    ))?;
1236
1237                let table_name = config
1238                    .table_name
1239                    .into_value(binding_name, "table_name")
1240                    .context(ErrorData::config_invalid(
1241                        binding_name,
1242                        "Failed to extract table_name from TableStorage binding",
1243                    ))?;
1244
1245                let client = AzureTableStorageClient::new(
1246                    crate::http_client::create_http_client(),
1247                    AzureTokenCache::new(azure_config.clone()),
1248                );
1249
1250                let kv_impl =
1251                    AzureTableStorageKv::new(client, resource_group_name, account_name, table_name);
1252                let kv: Arc<dyn Kv> = Arc::new(kv_impl);
1253                Ok(kv)
1254            }
1255            #[cfg(not(feature = "azure"))]
1256            KvBinding::TableStorage(_) => Err(AlienError::new(ErrorData::FeatureNotEnabled {
1257                feature: "azure".to_string(),
1258            })),
1259
1260            #[cfg(feature = "local")]
1261            KvBinding::Local(local_binding) => {
1262                use crate::providers::kv::local::LocalKv;
1263                use std::path::PathBuf;
1264
1265                // Get data directory from binding
1266                let data_dir = PathBuf::from(
1267                    local_binding
1268                        .data_dir
1269                        .into_value(binding_name, "data_dir")
1270                        .context(ErrorData::config_invalid(
1271                            binding_name,
1272                            "Failed to extract data_dir from Local binding",
1273                        ))?,
1274                );
1275
1276                // Create local disk-persisted KV implementation
1277                let kv_impl = LocalKv::new(data_dir).await?;
1278
1279                let kv: Arc<dyn Kv> = Arc::new(kv_impl);
1280                Ok(kv)
1281            }
1282            #[cfg(not(feature = "local"))]
1283            KvBinding::Local { .. } => Err(AlienError::new(ErrorData::FeatureNotEnabled {
1284                feature: "local".to_string(),
1285            })),
1286
1287            KvBinding::Redis(_) => Err(AlienError::new(ErrorData::UnsupportedBindingProvider {
1288                binding_name: binding_name.to_string(),
1289                env_var: binding_env_var(binding_name),
1290                provider: "redis".to_string(),
1291            })),
1292        }?;
1293
1294        self.put_cache("kv", binding_name, result.clone()).await;
1295        Ok(result)
1296    }
1297
1298    async fn load_postgres(&self, binding_name: &str) -> Result<Arc<dyn Postgres>> {
1299        let binding: PostgresBinding = self.parse_binding(binding_name, "Postgres")?;
1300        self.postgres.load(binding_name, &binding).await
1301    }
1302
1303    async fn load_queue(&self, binding_name: &str) -> Result<Arc<dyn Queue>> {
1304        if let Some(cached) = self
1305            .get_cached::<Arc<dyn Queue>>("queue", binding_name)
1306            .await
1307        {
1308            return Ok(cached);
1309        }
1310
1311        use alien_core::bindings::QueueBinding;
1312
1313        let binding: QueueBinding = self.parse_binding(binding_name, "Queue")?;
1314
1315        let result: Arc<dyn Queue> = match binding {
1316            #[cfg(feature = "aws")]
1317            QueueBinding::Sqs(config) => {
1318                use crate::providers::queue::aws_sqs::AwsSqsQueue;
1319
1320                let queue_url = config
1321                    .queue_url
1322                    .into_value(binding_name, "queue_url")
1323                    .context(ErrorData::config_invalid(
1324                        binding_name,
1325                        "Failed to extract queue_url from SQS binding",
1326                    ))?;
1327
1328                let aws_config = self.client_config.aws_config().ok_or_else(|| {
1329                    AlienError::new(ErrorData::ClientConfigInvalid {
1330                        platform: Platform::Aws,
1331                        message: "AWS config not available".to_string(),
1332                    })
1333                })?;
1334                let credentials =
1335                    alien_aws_clients::AwsCredentialProvider::from_config(aws_config.clone())
1336                        .await
1337                        .context(ErrorData::ClientConfigInvalid {
1338                            platform: Platform::Aws,
1339                            message: "Failed to create AWS credential provider".to_string(),
1340                        })?;
1341                let client = alien_aws_clients::sqs::SqsClient::new(
1342                    crate::http_client::create_http_client(),
1343                    credentials,
1344                );
1345                let q: Arc<dyn Queue> = Arc::new(AwsSqsQueue::new(queue_url, client));
1346                Ok(q)
1347            }
1348            #[cfg(not(feature = "aws"))]
1349            QueueBinding::Sqs(_) => Err(AlienError::new(ErrorData::FeatureNotEnabled {
1350                feature: "aws".to_string(),
1351            })),
1352
1353            #[cfg(feature = "gcp")]
1354            QueueBinding::Pubsub(config) => {
1355                use crate::providers::queue::gcp_pubsub::GcpPubSubQueue;
1356                let topic_name = config.topic.into_value(binding_name, "topic").context(
1357                    ErrorData::config_invalid(binding_name, "Failed to extract topic"),
1358                )?;
1359                let subscription_name = config
1360                    .subscription
1361                    .into_value(binding_name, "subscription")
1362                    .context(ErrorData::config_invalid(
1363                        binding_name,
1364                        "Failed to extract subscription",
1365                    ))?;
1366                let gcp_config = self.client_config.gcp_config().ok_or_else(|| {
1367                    AlienError::new(ErrorData::ClientConfigInvalid {
1368                        platform: Platform::Gcp,
1369                        message: "GCP config not available".to_string(),
1370                    })
1371                })?;
1372
1373                // Pass short names — PubSubClient methods prepend the project prefix
1374                let topic = if let Some(short) =
1375                    topic_name.strip_prefix(&format!("projects/{}/topics/", gcp_config.project_id))
1376                {
1377                    short.to_string()
1378                } else {
1379                    topic_name
1380                };
1381                let subscription = if let Some(short) = subscription_name.strip_prefix(&format!(
1382                    "projects/{}/subscriptions/",
1383                    gcp_config.project_id
1384                )) {
1385                    short.to_string()
1386                } else {
1387                    subscription_name
1388                };
1389
1390                let q: Arc<dyn Queue> =
1391                    Arc::new(GcpPubSubQueue::new(topic, subscription, gcp_config.clone()).await?);
1392                Ok(q)
1393            }
1394            #[cfg(not(feature = "gcp"))]
1395            QueueBinding::Pubsub(_) => Err(AlienError::new(ErrorData::FeatureNotEnabled {
1396                feature: "gcp".to_string(),
1397            })),
1398
1399            #[cfg(feature = "azure")]
1400            QueueBinding::Servicebus(config) => {
1401                use crate::providers::queue::azure_service_bus::AzureServiceBusQueue;
1402                let namespace = config
1403                    .namespace
1404                    .into_value(binding_name, "namespace")
1405                    .context(ErrorData::config_invalid(
1406                        binding_name,
1407                        "Failed to extract namespace",
1408                    ))?;
1409                let queue_name = config
1410                    .queue_name
1411                    .into_value(binding_name, "queue_name")
1412                    .context(ErrorData::config_invalid(
1413                        binding_name,
1414                        "Failed to extract queue_name",
1415                    ))?;
1416                let azure_config = self.client_config.azure_config().ok_or_else(|| {
1417                    AlienError::new(ErrorData::ClientConfigInvalid {
1418                        platform: Platform::Azure,
1419                        message: "Azure config not available".to_string(),
1420                    })
1421                })?;
1422                let q: Arc<dyn Queue> = Arc::new(
1423                    AzureServiceBusQueue::new(namespace, queue_name, azure_config.clone()).await?,
1424                );
1425                Ok(q)
1426            }
1427            #[cfg(not(feature = "azure"))]
1428            QueueBinding::Servicebus(_) => Err(AlienError::new(ErrorData::FeatureNotEnabled {
1429                feature: "azure".to_string(),
1430            })),
1431
1432            #[cfg(feature = "local")]
1433            QueueBinding::Local(config) => {
1434                use crate::providers::queue::local::LocalQueue;
1435
1436                let queue = LocalQueue::from_binding(config).await?;
1437                let q: Arc<dyn Queue> = Arc::new(queue);
1438                Ok(q)
1439            }
1440            #[cfg(not(feature = "local"))]
1441            QueueBinding::Local(_) => Err(AlienError::new(ErrorData::FeatureNotEnabled {
1442                feature: "local".to_string(),
1443            })),
1444        }?;
1445
1446        self.put_cache("queue", binding_name, result.clone()).await;
1447        Ok(result)
1448    }
1449
1450    async fn load_worker(&self, binding_name: &str) -> Result<Arc<dyn Worker>> {
1451        use alien_core::bindings::WorkerBinding;
1452
1453        let binding: WorkerBinding = self.parse_binding(binding_name, "worker")?;
1454
1455        match binding {
1456            #[cfg(feature = "aws")]
1457            WorkerBinding::Lambda(lambda_binding) => {
1458                use crate::providers::worker::LambdaWorker;
1459
1460                let aws_config = self.client_config.aws_config().ok_or_else(|| {
1461                    AlienError::new(ErrorData::ClientConfigInvalid {
1462                        platform: Platform::Aws,
1463                        message: "AWS config not available".to_string(),
1464                    })
1465                })?;
1466                let credentials =
1467                    alien_aws_clients::AwsCredentialProvider::from_config(aws_config.clone())
1468                        .await
1469                        .context(ErrorData::ClientConfigInvalid {
1470                            platform: Platform::Aws,
1471                            message: "Failed to create AWS credential provider".to_string(),
1472                        })?;
1473                let client = crate::http_client::create_http_client();
1474
1475                let function_impl = LambdaWorker::new(client, credentials, lambda_binding);
1476                let function: Arc<dyn Worker> = Arc::new(function_impl);
1477                Ok(function)
1478            }
1479            #[cfg(not(feature = "aws"))]
1480            WorkerBinding::Lambda(_) => Err(AlienError::new(ErrorData::FeatureNotEnabled {
1481                feature: "aws".to_string(),
1482            })),
1483
1484            #[cfg(feature = "gcp")]
1485            WorkerBinding::CloudRun(cloudrun_binding) => {
1486                use crate::providers::worker::CloudRunWorker;
1487
1488                let gcp_config = self.client_config.gcp_config().ok_or_else(|| {
1489                    AlienError::new(ErrorData::ClientConfigInvalid {
1490                        platform: Platform::Gcp,
1491                        message: "GCP config not available".to_string(),
1492                    })
1493                })?;
1494                let client = crate::http_client::create_http_client();
1495
1496                let function_impl =
1497                    CloudRunWorker::new(client, gcp_config.clone(), cloudrun_binding);
1498                let function: Arc<dyn Worker> = Arc::new(function_impl);
1499                Ok(function)
1500            }
1501            #[cfg(not(feature = "gcp"))]
1502            WorkerBinding::CloudRun(_) => Err(AlienError::new(ErrorData::FeatureNotEnabled {
1503                feature: "gcp".to_string(),
1504            })),
1505
1506            #[cfg(feature = "azure")]
1507            WorkerBinding::ContainerApp(container_app_binding) => {
1508                use crate::providers::worker::ContainerAppWorker;
1509
1510                let azure_config = self.client_config.azure_config().ok_or_else(|| {
1511                    AlienError::new(ErrorData::ClientConfigInvalid {
1512                        platform: Platform::Azure,
1513                        message: "Azure config not available".to_string(),
1514                    })
1515                })?;
1516                let client = crate::http_client::create_http_client();
1517
1518                let function_impl =
1519                    ContainerAppWorker::new(client, azure_config.clone(), container_app_binding);
1520                let function: Arc<dyn Worker> = Arc::new(function_impl);
1521                Ok(function)
1522            }
1523            #[cfg(not(feature = "azure"))]
1524            WorkerBinding::ContainerApp(_) => Err(AlienError::new(ErrorData::FeatureNotEnabled {
1525                feature: "azure".to_string(),
1526            })),
1527
1528            #[cfg(feature = "local")]
1529            WorkerBinding::Local(local_binding) => {
1530                use crate::providers::worker::LocalWorker;
1531
1532                let function_impl = LocalWorker::new(local_binding);
1533                let function: Arc<dyn Worker> = Arc::new(function_impl);
1534                Ok(function)
1535            }
1536            #[cfg(not(feature = "local"))]
1537            WorkerBinding::Local(_) => Err(AlienError::new(ErrorData::FeatureNotEnabled {
1538                feature: "local".to_string(),
1539            })),
1540
1541            #[cfg(feature = "kubernetes")]
1542            WorkerBinding::Kubernetes(kubernetes_binding) => {
1543                use crate::providers::worker::KubernetesWorker;
1544
1545                let function_impl =
1546                    KubernetesWorker::new(binding_name.to_string(), kubernetes_binding)?;
1547                let function: Arc<dyn Worker> = Arc::new(function_impl);
1548                Ok(function)
1549            }
1550            #[cfg(not(feature = "kubernetes"))]
1551            WorkerBinding::Kubernetes(_) => Err(AlienError::new(ErrorData::FeatureNotEnabled {
1552                feature: "kubernetes".to_string(),
1553            })),
1554        }
1555    }
1556
1557    async fn load_container(
1558        &self,
1559        binding_name: &str,
1560    ) -> Result<Arc<dyn crate::traits::Container>> {
1561        use alien_core::bindings::ContainerBinding;
1562
1563        let binding: ContainerBinding = self.parse_binding(binding_name, "container")?;
1564
1565        match binding {
1566            ContainerBinding::Horizon(horizon_binding) => {
1567                use crate::providers::container::HorizonContainer;
1568
1569                let container_impl = HorizonContainer::new(horizon_binding)?;
1570                let container: Arc<dyn crate::traits::Container> = Arc::new(container_impl);
1571                Ok(container)
1572            }
1573
1574            #[cfg(feature = "local")]
1575            ContainerBinding::Local(local_binding) => {
1576                use crate::providers::container::LocalContainer;
1577
1578                let container_impl = LocalContainer::new(local_binding)?;
1579                let container: Arc<dyn crate::traits::Container> = Arc::new(container_impl);
1580                Ok(container)
1581            }
1582            #[cfg(not(feature = "local"))]
1583            ContainerBinding::Local(_) => Err(AlienError::new(ErrorData::FeatureNotEnabled {
1584                feature: "local".to_string(),
1585            })),
1586
1587            #[cfg(feature = "kubernetes")]
1588            ContainerBinding::Kubernetes(kubernetes_binding) => {
1589                use crate::providers::container::KubernetesContainer;
1590
1591                let container_impl =
1592                    KubernetesContainer::new(binding_name.to_string(), kubernetes_binding)?;
1593                let container: Arc<dyn crate::traits::Container> = Arc::new(container_impl);
1594                Ok(container)
1595            }
1596            #[cfg(not(feature = "kubernetes"))]
1597            ContainerBinding::Kubernetes(_) => Err(AlienError::new(ErrorData::FeatureNotEnabled {
1598                feature: "kubernetes".to_string(),
1599            })),
1600        }
1601    }
1602
1603    async fn load_service_account(
1604        &self,
1605        binding_name: &str,
1606    ) -> Result<Arc<dyn crate::traits::ServiceAccount>> {
1607        use alien_core::bindings::ServiceAccountBinding;
1608
1609        let binding: ServiceAccountBinding = self.parse_binding(binding_name, "service account")?;
1610
1611        match binding {
1612            #[cfg(feature = "aws")]
1613            ServiceAccountBinding::AwsIam(aws_binding) => {
1614                use crate::providers::service_account::aws_iam::AwsIamServiceAccount;
1615
1616                let aws_config = self.client_config.aws_config().ok_or_else(|| {
1617                    AlienError::new(ErrorData::ClientConfigInvalid {
1618                        platform: Platform::Aws,
1619                        message: "AWS config not available".to_string(),
1620                    })
1621                })?;
1622                let client = crate::http_client::create_http_client();
1623
1624                let service_account_impl =
1625                    AwsIamServiceAccount::new(client, aws_config.clone(), aws_binding);
1626                let service_account: Arc<dyn crate::traits::ServiceAccount> =
1627                    Arc::new(service_account_impl);
1628                Ok(service_account)
1629            }
1630            #[cfg(not(feature = "aws"))]
1631            ServiceAccountBinding::AwsIam(_) => {
1632                Err(AlienError::new(ErrorData::FeatureNotEnabled {
1633                    feature: "aws".to_string(),
1634                }))
1635            }
1636
1637            #[cfg(feature = "gcp")]
1638            ServiceAccountBinding::GcpServiceAccount(gcp_binding) => {
1639                use crate::providers::service_account::gcp_service_account::GcpServiceAccount;
1640
1641                let gcp_config = self.client_config.gcp_config().ok_or_else(|| {
1642                    AlienError::new(ErrorData::ClientConfigInvalid {
1643                        platform: Platform::Gcp,
1644                        message: "GCP config not available".to_string(),
1645                    })
1646                })?;
1647                let client = crate::http_client::create_http_client();
1648
1649                let service_account_impl =
1650                    GcpServiceAccount::new(client, gcp_config.clone(), gcp_binding);
1651                let service_account: Arc<dyn crate::traits::ServiceAccount> =
1652                    Arc::new(service_account_impl);
1653                Ok(service_account)
1654            }
1655            #[cfg(not(feature = "gcp"))]
1656            ServiceAccountBinding::GcpServiceAccount(_) => {
1657                Err(AlienError::new(ErrorData::FeatureNotEnabled {
1658                    feature: "gcp".to_string(),
1659                }))
1660            }
1661
1662            #[cfg(feature = "azure")]
1663            ServiceAccountBinding::AzureManagedIdentity(azure_binding) => {
1664                use crate::providers::service_account::azure_managed_identity::AzureManagedIdentityServiceAccount;
1665
1666                let azure_config = self.client_config.azure_config().ok_or_else(|| {
1667                    AlienError::new(ErrorData::ClientConfigInvalid {
1668                        platform: Platform::Azure,
1669                        message: "Azure config not available".to_string(),
1670                    })
1671                })?;
1672
1673                let service_account_impl =
1674                    AzureManagedIdentityServiceAccount::new(azure_config.clone(), azure_binding);
1675                let service_account: Arc<dyn crate::traits::ServiceAccount> =
1676                    Arc::new(service_account_impl);
1677                Ok(service_account)
1678            }
1679            #[cfg(not(feature = "azure"))]
1680            ServiceAccountBinding::AzureManagedIdentity(_) => {
1681                Err(AlienError::new(ErrorData::FeatureNotEnabled {
1682                    feature: "azure".to_string(),
1683                }))
1684            }
1685        }
1686    }
1687}
1688
1689#[cfg(test)]
1690mod tests {
1691    use super::*;
1692    use alien_core::ENV_ALIEN_DEPLOYMENT_TYPE;
1693
1694    fn kubernetes_aws_env() -> HashMap<String, String> {
1695        HashMap::from([
1696            (
1697                ENV_ALIEN_DEPLOYMENT_TYPE.to_string(),
1698                Platform::Kubernetes.as_str().to_string(),
1699            ),
1700            (
1701                ENV_OPERATOR_BASE_PLATFORM.to_string(),
1702                Platform::Aws.as_str().to_string(),
1703            ),
1704            (
1705                "KUBERNETES_SERVICE_HOST".to_string(),
1706                "10.0.0.1".to_string(),
1707            ),
1708            ("KUBERNETES_SERVICE_PORT".to_string(), "443".to_string()),
1709            ("AWS_REGION".to_string(), "us-east-1".to_string()),
1710            ("AWS_ACCOUNT_ID".to_string(), "123456789012".to_string()),
1711            ("AWS_ACCESS_KEY_ID".to_string(), "test".to_string()),
1712            ("AWS_SECRET_ACCESS_KEY".to_string(), "test".to_string()),
1713        ])
1714    }
1715
1716    #[cfg(feature = "kubernetes")]
1717    fn kubernetes_azure_env() -> HashMap<String, String> {
1718        HashMap::from([
1719            (
1720                ENV_ALIEN_DEPLOYMENT_TYPE.to_string(),
1721                Platform::Kubernetes.as_str().to_string(),
1722            ),
1723            (
1724                ENV_OPERATOR_BASE_PLATFORM.to_string(),
1725                Platform::Azure.as_str().to_string(),
1726            ),
1727            (
1728                "KUBERNETES_SERVICE_HOST".to_string(),
1729                "10.0.0.1".to_string(),
1730            ),
1731            ("KUBERNETES_SERVICE_PORT".to_string(), "443".to_string()),
1732            (
1733                "AZURE_SUBSCRIPTION_ID".to_string(),
1734                "00000000-0000-0000-0000-000000000000".to_string(),
1735            ),
1736            (
1737                "AZURE_TENANT_ID".to_string(),
1738                "11111111-1111-1111-1111-111111111111".to_string(),
1739            ),
1740            ("AZURE_REGION".to_string(), "eastus".to_string()),
1741            (
1742                "AZURE_CLIENT_ID".to_string(),
1743                "22222222-2222-2222-2222-222222222222".to_string(),
1744            ),
1745            (
1746                "AZURE_FEDERATED_TOKEN_FILE".to_string(),
1747                "/var/run/secrets/azure/tokens/azure-identity-token".to_string(),
1748            ),
1749            (
1750                "AZURE_AUTHORITY_HOST".to_string(),
1751                "https://login.microsoftonline.com/".to_string(),
1752            ),
1753        ])
1754    }
1755
1756    #[tokio::test]
1757    async fn lazy_env_provider_defers_cloud_client_config_until_binding_use() {
1758        let env = HashMap::from([
1759            (
1760                ENV_ALIEN_DEPLOYMENT_TYPE.to_string(),
1761                Platform::Aws.as_str().to_string(),
1762            ),
1763            ("AWS_EC2_METADATA_DISABLED".to_string(), "true".to_string()),
1764            (
1765                "AWS_PROFILE".to_string(),
1766                "__alien_missing_test_profile__".to_string(),
1767            ),
1768            (
1769                "ALIEN_SECRETS_BINDING".to_string(),
1770                r#"{"service":"parameter-store","vaultPrefix":"test-secrets"}"#.to_string(),
1771            ),
1772        ]);
1773
1774        let provider = BindingsProvider::from_env_lazy(env)
1775            .expect("lazy provider construction should validate binding JSON without AWS config");
1776
1777        let error = provider
1778            .load_vault("secrets")
1779            .await
1780            .expect_err("binding use should still require AWS client config");
1781
1782        assert_eq!(error.code, "CLIENT_CONFIG_INVALID");
1783    }
1784
1785    /// The construction-time binding-JSON parse is load-bearing: `select`
1786    /// reuses it instead of re-parsing `env` on first use, so malformed JSON
1787    /// must fail at construction — for BOTH lazy constructors.
1788    #[test]
1789    fn malformed_binding_json_fails_at_construction_for_both_lazy_constructors() {
1790        let env = HashMap::from([
1791            (
1792                ENV_ALIEN_DEPLOYMENT_TYPE.to_string(),
1793                Platform::Aws.as_str().to_string(),
1794            ),
1795            ("ALIEN_FILES_BINDING".to_string(), "not-json".to_string()),
1796        ]);
1797
1798        let error = BindingsProvider::from_env_lazy(env.clone())
1799            .expect_err("from_env_lazy must reject malformed binding JSON at construction");
1800        assert_eq!(error.code, "BINDING_CONFIG_INVALID");
1801
1802        let error = BindingsProvider::from_env_deferred(env)
1803            .expect_err("from_env_deferred must reject malformed binding JSON at construction");
1804        assert_eq!(error.code, "BINDING_CONFIG_INVALID");
1805    }
1806
1807    // Building the KubernetesCloud client config requires kubernetes support
1808    // to be compiled in; without the feature, `from_env` rejects the config.
1809    #[cfg(feature = "kubernetes")]
1810    #[tokio::test]
1811    async fn from_env_builds_kubernetes_cloud_config_when_base_platform_is_set() {
1812        let provider = BindingsProvider::from_env(kubernetes_aws_env())
1813            .await
1814            .unwrap();
1815
1816        assert!(provider.client_config.kubernetes_config().is_some());
1817        assert!(provider.client_config.aws_config().is_some());
1818        assert!(matches!(
1819            provider.client_config,
1820            ClientConfig::KubernetesCloud { .. }
1821        ));
1822    }
1823
1824    #[cfg(feature = "kubernetes")]
1825    #[tokio::test]
1826    async fn from_env_builds_kubernetes_cloud_config_for_azure_workload_identity() {
1827        let provider = BindingsProvider::from_env(kubernetes_azure_env())
1828            .await
1829            .unwrap();
1830
1831        assert!(provider.client_config.kubernetes_config().is_some());
1832        assert!(provider.client_config.azure_config().is_some());
1833        assert!(matches!(
1834            provider.client_config,
1835            ClientConfig::KubernetesCloud { .. }
1836        ));
1837    }
1838
1839    #[tokio::test]
1840    async fn from_env_rejects_non_cloud_kubernetes_base_platform() {
1841        let mut env = kubernetes_aws_env();
1842        env.insert(
1843            ENV_OPERATOR_BASE_PLATFORM.to_string(),
1844            Platform::Kubernetes.as_str().to_string(),
1845        );
1846
1847        let error = BindingsProvider::from_env(env).await.unwrap_err();
1848
1849        assert!(error.to_string().contains(ENV_OPERATOR_BASE_PLATFORM));
1850    }
1851
1852    #[tokio::test]
1853    async fn load_storage_for_unconfigured_binding_returns_binding_not_configured() {
1854        let env = HashMap::from([(
1855            ENV_ALIEN_DEPLOYMENT_TYPE.to_string(),
1856            Platform::Local.as_str().to_string(),
1857        )]);
1858        let provider = BindingsProvider::from_env(env)
1859            .await
1860            .expect("provider with no bindings configured should still construct");
1861
1862        let error = provider
1863            .load_storage("files")
1864            .await
1865            .expect_err("binding that was never configured should error");
1866
1867        assert_eq!(error.code, "BINDING_NOT_CONFIGURED");
1868        assert!(
1869            error.to_string().contains("ALIEN_FILES_BINDING"),
1870            "message should name the derived env var, got: {error}"
1871        );
1872    }
1873
1874    #[tokio::test]
1875    async fn load_kv_for_malformed_binding_json_returns_binding_config_invalid_with_env_var() {
1876        let env = HashMap::from([
1877            (
1878                ENV_ALIEN_DEPLOYMENT_TYPE.to_string(),
1879                Platform::Local.as_str().to_string(),
1880            ),
1881            (
1882                "ALIEN_CACHE_BINDING".to_string(),
1883                r#"{"service":"local-kv"}"#.to_string(), // missing required dataDir field
1884            ),
1885        ]);
1886        let provider = BindingsProvider::from_env(env)
1887            .await
1888            .expect("provider construction only validates JSON parses, not field completeness");
1889
1890        let error = provider
1891            .load_kv("cache")
1892            .await
1893            .expect_err("binding missing a required field should error");
1894
1895        assert_eq!(error.code, "BINDING_CONFIG_INVALID");
1896        assert!(
1897            error.to_string().contains("ALIEN_CACHE_BINDING"),
1898            "message should name the env var, got: {error}"
1899        );
1900    }
1901
1902    // --- Selection order on the lazy path (native-vs-mint) ---
1903
1904    mod selection {
1905        use super::*;
1906        use crate::traits::BindingsProviderApi;
1907        use alien_core::{
1908            ENV_ALIEN_DEPLOYMENT_ID, ENV_ALIEN_DEPLOYMENT_SERVICE_ACCOUNT,
1909            ENV_ALIEN_DEPLOYMENT_TOKEN, ENV_ALIEN_MANAGER_URL, ENV_ALIEN_RESOURCE_ID,
1910        };
1911        use axum::{extract::State, routing::post, Json, Router};
1912        use std::net::SocketAddr;
1913        use std::sync::atomic::{AtomicUsize, Ordering};
1914        use tempfile::TempDir;
1915
1916        /// Fake mint endpoint that counts requests and returns a `Local` config.
1917        async fn mint_handler(State(calls): State<Arc<AtomicUsize>>) -> Json<serde_json::Value> {
1918            calls.fetch_add(1, Ordering::SeqCst);
1919            let expires_at = (chrono::Utc::now() + chrono::Duration::seconds(3600)).to_rfc3339();
1920            Json(serde_json::json!({
1921                "clientConfig": { "platform": "local", "state_directory": "/tmp/alien-sel-test" },
1922                "expiresAt": expires_at,
1923                "principal": "local:mint-test",
1924            }))
1925        }
1926
1927        async fn spawn_mint_server() -> (String, Arc<AtomicUsize>) {
1928            let calls = Arc::new(AtomicUsize::new(0));
1929            let app = Router::new()
1930                .route("/v1/credentials/mint", post(mint_handler))
1931                .with_state(calls.clone());
1932            let listener = tokio::net::TcpListener::bind(SocketAddr::from(([127, 0, 0, 1], 0)))
1933                .await
1934                .expect("bind");
1935            let addr = listener.local_addr().expect("addr");
1936            tokio::spawn(async move {
1937                axum::serve(listener, app).await.expect("serve");
1938            });
1939            (format!("http://{addr}"), calls)
1940        }
1941
1942        fn local_storage_binding(dir: &TempDir) -> String {
1943            format!(
1944                r#"{{"service":"local-storage","storagePath":"{}"}}"#,
1945                dir.path().display()
1946            )
1947        }
1948
1949        /// Full mint env contract pointing at `manager_url`.
1950        fn mint_env(manager_url: &str) -> HashMap<String, String> {
1951            HashMap::from([
1952                (ENV_ALIEN_MANAGER_URL.to_string(), manager_url.to_string()),
1953                (
1954                    ENV_ALIEN_DEPLOYMENT_TOKEN.to_string(),
1955                    "ax_deploy_tok".to_string(),
1956                ),
1957                (ENV_ALIEN_DEPLOYMENT_ID.to_string(), "dep_1".to_string()),
1958                (
1959                    ENV_ALIEN_DEPLOYMENT_SERVICE_ACCOUNT.to_string(),
1960                    "management".to_string(),
1961                ),
1962                (ENV_ALIEN_RESOURCE_ID.to_string(), "api".to_string()),
1963            ])
1964        }
1965
1966        #[tokio::test]
1967        async fn native_config_wins_and_never_mints() {
1968            // Local platform resolves `ClientConfig::from_env` successfully, so
1969            // even with a full mint contract present the resolver must pick the
1970            // native path and never call the manager.
1971            let (base_url, calls) = spawn_mint_server().await;
1972            let dir = TempDir::new().expect("tempdir");
1973
1974            let mut env = mint_env(&base_url);
1975            env.insert(
1976                ENV_ALIEN_DEPLOYMENT_TYPE.to_string(),
1977                Platform::Local.as_str().to_string(),
1978            );
1979            env.insert(
1980                "ALIEN_FILES_BINDING".to_string(),
1981                local_storage_binding(&dir),
1982            );
1983
1984            let provider = BindingsProvider::from_env_lazy(env).expect("lazy construct");
1985            provider
1986                .load_storage("files")
1987                .await
1988                .expect("native local storage should load");
1989
1990            assert_eq!(
1991                calls.load(Ordering::SeqCst),
1992                0,
1993                "native credentials must never trigger a mint"
1994            );
1995        }
1996
1997        #[tokio::test]
1998        async fn mints_when_native_config_unavailable() {
1999            // AWS platform with no usable credentials makes `from_env` fail; with
2000            // the mint contract present the resolver falls through to minting and
2001            // resolves the (Local) minted config, which then serves the binding.
2002            let (base_url, calls) = spawn_mint_server().await;
2003            let dir = TempDir::new().expect("tempdir");
2004
2005            let mut env = mint_env(&base_url);
2006            env.insert(
2007                ENV_ALIEN_DEPLOYMENT_TYPE.to_string(),
2008                Platform::Aws.as_str().to_string(),
2009            );
2010            env.insert("AWS_EC2_METADATA_DISABLED".to_string(), "true".to_string());
2011            env.insert(
2012                "AWS_PROFILE".to_string(),
2013                "__alien_missing_test_profile__".to_string(),
2014            );
2015            env.insert(
2016                "ALIEN_FILES_BINDING".to_string(),
2017                local_storage_binding(&dir),
2018            );
2019
2020            let provider = BindingsProvider::from_env_lazy(env).expect("lazy construct");
2021            provider
2022                .load_storage("files")
2023                .await
2024                .expect("mint path should resolve a usable config");
2025
2026            assert_eq!(
2027                calls.load(Ordering::SeqCst),
2028                1,
2029                "unavailable native credentials must trigger exactly one mint"
2030            );
2031        }
2032
2033        #[tokio::test]
2034        async fn no_mint_contract_preserves_original_from_env_error() {
2035            // AWS platform, no usable creds, and no mint contract: the resolver
2036            // must surface the original `from_env` error unchanged (path 3).
2037            let dir = TempDir::new().expect("tempdir");
2038            let env = HashMap::from([
2039                (
2040                    ENV_ALIEN_DEPLOYMENT_TYPE.to_string(),
2041                    Platform::Aws.as_str().to_string(),
2042                ),
2043                ("AWS_EC2_METADATA_DISABLED".to_string(), "true".to_string()),
2044                (
2045                    "AWS_PROFILE".to_string(),
2046                    "__alien_missing_test_profile__".to_string(),
2047                ),
2048                (
2049                    "ALIEN_FILES_BINDING".to_string(),
2050                    local_storage_binding(&dir),
2051                ),
2052            ]);
2053
2054            let provider = BindingsProvider::from_env_lazy(env).expect("lazy construct");
2055            let error = provider
2056                .load_storage("files")
2057                .await
2058                .expect_err("no creds and no mint contract must error");
2059
2060            assert_eq!(error.code, "CLIENT_CONFIG_INVALID");
2061        }
2062    }
2063}