Skip to main content

alien_bindings/
provider.rs

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