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