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