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