Skip to main content

alien_bindings/
provider.rs

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