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