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