1use crate::{
4 error::{binding_env_var, ErrorData, Result},
5 providers::postgres::runtime::PostgresRuntime,
6 traits::{
7 ArtifactRegistry, BindingsProviderApi, Build, Container, Key, 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_key(&self, binding_name: &str) -> Result<Arc<dyn Key>> {
445 self.ensure_binding_present(binding_name)?;
446 self.provider().await?.load_key(binding_name).await
447 }
448
449 async fn load_build(&self, binding_name: &str) -> Result<Arc<dyn Build>> {
450 self.ensure_binding_present(binding_name)?;
451 self.provider().await?.load_build(binding_name).await
452 }
453
454 async fn load_artifact_registry(
455 &self,
456 binding_name: &str,
457 ) -> Result<Arc<dyn ArtifactRegistry>> {
458 self.ensure_binding_present(binding_name)?;
459 self.provider()
460 .await?
461 .load_artifact_registry(binding_name)
462 .await
463 }
464
465 async fn load_vault(&self, binding_name: &str) -> Result<Arc<dyn Vault>> {
466 self.ensure_binding_present(binding_name)?;
467 self.provider().await?.load_vault(binding_name).await
468 }
469
470 async fn load_kv(&self, binding_name: &str) -> Result<Arc<dyn Kv>> {
471 self.ensure_binding_present(binding_name)?;
472 self.provider().await?.load_kv(binding_name).await
473 }
474
475 async fn load_postgres(&self, binding_name: &str) -> Result<Arc<dyn Postgres>> {
476 self.ensure_binding_present(binding_name)?;
477 self.provider().await?.load_postgres(binding_name).await
478 }
479
480 async fn load_queue(&self, binding_name: &str) -> Result<Arc<dyn Queue>> {
481 self.ensure_binding_present(binding_name)?;
482 self.provider().await?.load_queue(binding_name).await
483 }
484
485 async fn load_worker(&self, binding_name: &str) -> Result<Arc<dyn Worker>> {
486 self.ensure_binding_present(binding_name)?;
487 self.provider().await?.load_worker(binding_name).await
488 }
489
490 async fn load_container(&self, binding_name: &str) -> Result<Arc<dyn Container>> {
491 self.ensure_binding_present(binding_name)?;
492 self.provider().await?.load_container(binding_name).await
493 }
494
495 async fn load_service_account(&self, binding_name: &str) -> Result<Arc<dyn ServiceAccount>> {
496 self.ensure_binding_present(binding_name)?;
497 self.provider()
498 .await?
499 .load_service_account(binding_name)
500 .await
501 }
502
503 async fn load_sandbox(&self, binding_name: &str) -> Result<Arc<dyn crate::traits::Sandbox>> {
504 self.ensure_binding_present(binding_name)?;
505 self.provider().await?.load_sandbox(binding_name).await
506 }
507}
508
509#[async_trait]
510impl BindingsProviderApi for BindingsProvider {
511 async fn load_storage(&self, binding_name: &str) -> Result<Arc<dyn Storage>> {
512 if let Some(cached) = self
513 .get_cached::<Arc<dyn Storage>>("storage", binding_name)
514 .await
515 {
516 return Ok(cached);
517 }
518
519 use alien_core::bindings::StorageBinding;
520
521 let binding: StorageBinding = self.parse_binding(binding_name, "storage")?;
523
524 let result: Arc<dyn Storage> = match binding {
525 #[cfg(feature = "aws")]
526 StorageBinding::S3(config) => {
527 use crate::providers::storage::aws_s3::S3Storage;
528
529 let aws_config = self.client_config.aws_config().ok_or_else(|| {
531 AlienError::new(ErrorData::ClientConfigInvalid {
532 platform: Platform::Aws,
533 message: "AWS config not available".to_string(),
534 })
535 })?;
536
537 let credentials =
538 alien_aws_clients::AwsCredentialProvider::from_config(aws_config.clone())
539 .await
540 .context(ErrorData::BindingSetupFailed {
541 binding_type: "AWS S3 storage".to_string(),
542 reason: "Failed to create credential provider".to_string(),
543 })?;
544
545 let bucket_name = config
547 .bucket_name
548 .into_value(binding_name, "bucket_name")
549 .context(ErrorData::config_invalid(
550 binding_name,
551 "Failed to extract bucket_name from S3 binding",
552 ))?;
553
554 let storage: Arc<dyn Storage> = Arc::new(S3Storage::new(bucket_name, credentials)?);
555 Ok(storage)
556 }
557 #[cfg(not(feature = "aws"))]
558 StorageBinding::S3 { .. } => Err(AlienError::new(ErrorData::FeatureNotEnabled {
559 feature: "aws".to_string(),
560 })),
561
562 #[cfg(feature = "azure")]
563 StorageBinding::Blob(config) => {
564 use crate::providers::storage::azure_blob::BlobStorage;
565
566 let azure_config = self.client_config.azure_config().ok_or_else(|| {
567 AlienError::new(ErrorData::ClientConfigInvalid {
568 platform: Platform::Azure,
569 message: "Azure config not available".to_string(),
570 })
571 })?;
572
573 let container_name = config
575 .container_name
576 .into_value(binding_name, "container_name")
577 .context(ErrorData::config_invalid(
578 binding_name,
579 "Failed to extract container_name from Blob binding",
580 ))?;
581
582 let account_name = config
583 .account_name
584 .into_value(binding_name, "account_name")
585 .context(ErrorData::config_invalid(
586 binding_name,
587 "Failed to extract account_name from Blob binding",
588 ))?;
589
590 let storage: Arc<dyn Storage> = Arc::new(BlobStorage::new(
591 container_name,
592 account_name,
593 azure_config,
594 )?);
595 Ok(storage)
596 }
597 #[cfg(not(feature = "azure"))]
598 StorageBinding::Blob { .. } => Err(AlienError::new(ErrorData::FeatureNotEnabled {
599 feature: "azure".to_string(),
600 })),
601
602 #[cfg(feature = "gcp")]
603 StorageBinding::Gcs(config) => {
604 use crate::providers::storage::gcp_gcs::GcsStorage;
605
606 let gcp_config = self.client_config.gcp_config().ok_or_else(|| {
607 AlienError::new(ErrorData::ClientConfigInvalid {
608 platform: Platform::Gcp,
609 message: "GCP config not available".to_string(),
610 })
611 })?;
612
613 let bucket_name = config
615 .bucket_name
616 .into_value(binding_name, "bucket_name")
617 .context(ErrorData::config_invalid(
618 binding_name,
619 "Failed to extract bucket_name from Gcs binding",
620 ))?;
621
622 let storage: Arc<dyn Storage> = Arc::new(GcsStorage::new(bucket_name, gcp_config)?);
623 Ok(storage)
624 }
625 #[cfg(not(feature = "gcp"))]
626 StorageBinding::Gcs { .. } => Err(AlienError::new(ErrorData::FeatureNotEnabled {
627 feature: "gcp".to_string(),
628 })),
629
630 #[cfg(feature = "local")]
631 StorageBinding::Local(config) => {
632 use crate::providers::storage::local::LocalStorage;
633
634 let storage_path = config
636 .storage_path
637 .into_value(binding_name, "storage_path")
638 .context(ErrorData::config_invalid(
639 binding_name,
640 "Failed to extract storage_path from Local binding",
641 ))?;
642
643 let storage: Arc<dyn Storage> = Arc::new(LocalStorage::new(storage_path)?);
644 Ok(storage)
645 }
646 #[cfg(not(feature = "local"))]
647 StorageBinding::Local { .. } => Err(AlienError::new(ErrorData::FeatureNotEnabled {
648 feature: "local".to_string(),
649 })),
650 }?;
651
652 self.put_cache("storage", binding_name, result.clone())
653 .await;
654 Ok(result)
655 }
656
657 async fn load_key(&self, binding_name: &str) -> Result<Arc<dyn Key>> {
658 if let Some(cached) = self.get_cached::<Arc<dyn Key>>("key", binding_name).await {
659 return Ok(cached);
660 }
661
662 use alien_core::bindings::KeyBinding;
663 let binding: KeyBinding = self.parse_binding(binding_name, "key")?;
664 let key: Arc<dyn Key> = match binding {
665 #[cfg(feature = "aws")]
666 KeyBinding::AwsKms(config) => {
667 use crate::providers::key::aws::AwsKmsKey;
668 use alien_aws_clients::kms::KmsClient;
669 let mut aws_config = self.client_config.aws_config().cloned().ok_or_else(|| {
670 AlienError::new(ErrorData::ClientConfigInvalid {
671 platform: Platform::Aws,
672 message: "AWS config not available".to_string(),
673 })
674 })?;
675 if let Some(region) = config.region {
676 aws_config.region = region.into_value(binding_name, "region").context(
677 ErrorData::config_invalid(
678 binding_name,
679 "Failed to extract region from KMS binding",
680 ),
681 )?;
682 }
683 let credentials = alien_aws_clients::AwsCredentialProvider::from_config(aws_config)
684 .await
685 .context(ErrorData::BindingSetupFailed {
686 binding_type: "AWS KMS key".to_string(),
687 reason: "Failed to create credential provider".to_string(),
688 })?;
689 let key_arn = config.key_arn.into_value(binding_name, "key_arn").context(
690 ErrorData::config_invalid(
691 binding_name,
692 "Failed to extract key_arn from KMS binding",
693 ),
694 )?;
695 Arc::new(AwsKmsKey::new(
696 Arc::new(KmsClient::new(
697 crate::http_client::create_http_client(),
698 credentials,
699 )),
700 key_arn,
701 ))
702 }
703 #[cfg(not(feature = "aws"))]
704 KeyBinding::AwsKms(_) => {
705 return Err(AlienError::new(ErrorData::FeatureNotEnabled {
706 feature: "aws".to_string(),
707 }))
708 }
709 #[cfg(feature = "gcp")]
710 KeyBinding::GcpCloudKms(config) => {
711 use crate::providers::key::gcp::GcpCloudKmsKey;
712 use alien_gcp_clients::cloud_kms::CloudKmsClient;
713 let gcp_config = self.client_config.gcp_config().ok_or_else(|| {
714 AlienError::new(ErrorData::ClientConfigInvalid {
715 platform: Platform::Gcp,
716 message: "GCP config not available".to_string(),
717 })
718 })?;
719 let crypto_key_name = config
720 .crypto_key_name
721 .into_value(binding_name, "crypto_key_name")
722 .context(ErrorData::config_invalid(
723 binding_name,
724 "Failed to extract crypto_key_name from Cloud KMS binding",
725 ))?;
726 Arc::new(GcpCloudKmsKey::new(
727 Arc::new(CloudKmsClient::new(
728 crate::http_client::create_http_client(),
729 gcp_config.clone(),
730 )),
731 crypto_key_name,
732 ))
733 }
734 #[cfg(not(feature = "gcp"))]
735 KeyBinding::GcpCloudKms(_) => {
736 return Err(AlienError::new(ErrorData::FeatureNotEnabled {
737 feature: "gcp".to_string(),
738 }))
739 }
740 #[cfg(feature = "azure")]
741 KeyBinding::AzureKeyVault(config) => {
742 use crate::providers::key::azure::AzureKeyVaultKey;
743 use alien_azure_clients::keyvault::AzureKeyVaultKeysClient;
744 use alien_azure_clients::AzureTokenCache;
745 let azure_config = self.client_config.azure_config().ok_or_else(|| {
746 AlienError::new(ErrorData::ClientConfigInvalid {
747 platform: Platform::Azure,
748 message: "Azure config not available".to_string(),
749 })
750 })?;
751 let key_id = config.key_id.into_value(binding_name, "key_id").context(
752 ErrorData::config_invalid(
753 binding_name,
754 "Failed to extract key_id from Key Vault binding",
755 ),
756 )?;
757 Arc::new(AzureKeyVaultKey::new(
758 Arc::new(AzureKeyVaultKeysClient::new(
759 crate::http_client::create_http_client(),
760 AzureTokenCache::new(azure_config.clone()),
761 )),
762 key_id,
763 ))
764 }
765 #[cfg(not(feature = "azure"))]
766 KeyBinding::AzureKeyVault(_) => {
767 return Err(AlienError::new(ErrorData::FeatureNotEnabled {
768 feature: "azure".to_string(),
769 }))
770 }
771 };
772
773 self.put_cache("key", binding_name, key.clone()).await;
774 Ok(key)
775 }
776
777 async fn load_build(&self, binding_name: &str) -> Result<Arc<dyn Build>> {
778 use alien_core::bindings::BuildBinding;
779
780 let binding: BuildBinding = self.parse_binding(binding_name, "build")?;
781
782 match binding {
783 #[cfg(feature = "aws")]
784 BuildBinding::Codebuild { .. } => {
785 use crate::providers::build::codebuild::CodebuildBuild;
786
787 let aws_config = self.client_config.aws_config().ok_or_else(|| {
788 AlienError::new(ErrorData::ClientConfigInvalid {
789 platform: Platform::Aws,
790 message: "AWS config not available".to_string(),
791 })
792 })?;
793 let credentials =
794 alien_aws_clients::AwsCredentialProvider::from_config(aws_config.clone())
795 .await
796 .context(ErrorData::ClientConfigInvalid {
797 platform: Platform::Aws,
798 message: "Failed to create AWS credential provider".to_string(),
799 })?;
800
801 let build = Arc::new(
802 CodebuildBuild::new(binding_name.to_string(), binding, &credentials)
803 .await
804 .context(ErrorData::config_invalid(
805 binding_name,
806 "Failed to initialize AWS CodeBuild client",
807 ))?,
808 );
809 Ok(build)
810 }
811 #[cfg(not(feature = "aws"))]
812 BuildBinding::Codebuild { .. } => Err(AlienError::new(ErrorData::FeatureNotEnabled {
813 feature: "aws".to_string(),
814 })),
815
816 #[cfg(feature = "azure")]
817 BuildBinding::Aca { .. } => {
818 use crate::providers::build::aca::AcaBuild;
819
820 let azure_config = self.client_config.azure_config().ok_or_else(|| {
821 AlienError::new(ErrorData::ClientConfigInvalid {
822 platform: Platform::Azure,
823 message: "Azure config not available".to_string(),
824 })
825 })?;
826
827 let build = Arc::new(
828 AcaBuild::new(binding_name.to_string(), binding, azure_config)
829 .await
830 .context(ErrorData::config_invalid(
831 binding_name,
832 "Failed to initialize Azure Container Apps build",
833 ))?,
834 );
835 Ok(build)
836 }
837 #[cfg(not(feature = "azure"))]
838 BuildBinding::Aca { .. } => Err(AlienError::new(ErrorData::FeatureNotEnabled {
839 feature: "azure".to_string(),
840 })),
841
842 #[cfg(feature = "gcp")]
843 BuildBinding::Cloudbuild { .. } => {
844 use crate::providers::build::cloudbuild::CloudbuildBuild;
845
846 let gcp_config = self.client_config.gcp_config().ok_or_else(|| {
847 AlienError::new(ErrorData::ClientConfigInvalid {
848 platform: Platform::Gcp,
849 message: "GCP config not available".to_string(),
850 })
851 })?;
852
853 let build = Arc::new(
854 CloudbuildBuild::new(binding_name.to_string(), binding, gcp_config)
855 .await
856 .context(ErrorData::config_invalid(
857 binding_name,
858 "Failed to initialize GCP Cloud Build client",
859 ))?,
860 );
861 Ok(build)
862 }
863 #[cfg(not(feature = "gcp"))]
864 BuildBinding::Cloudbuild { .. } => Err(AlienError::new(ErrorData::FeatureNotEnabled {
865 feature: "gcp".to_string(),
866 })),
867
868 #[cfg(feature = "local")]
869 BuildBinding::Local { .. } => {
870 use crate::providers::build::local::LocalBuild;
871
872 let build = Arc::new(LocalBuild::new(binding_name.to_string(), binding)?);
873 Ok(build)
874 }
875 #[cfg(not(feature = "local"))]
876 BuildBinding::Local { .. } => Err(AlienError::new(ErrorData::FeatureNotEnabled {
877 feature: "local".to_string(),
878 })),
879
880 #[cfg(feature = "kubernetes")]
881 BuildBinding::Kubernetes { .. } => {
882 use crate::providers::build::kubernetes::KubernetesBuild;
883
884 let build =
885 Arc::new(KubernetesBuild::new(binding_name.to_string(), binding).await?);
886 Ok(build)
887 }
888 #[cfg(not(feature = "kubernetes"))]
889 BuildBinding::Kubernetes { .. } => Err(AlienError::new(ErrorData::FeatureNotEnabled {
890 feature: "kubernetes".to_string(),
891 })),
892 }
893 }
894
895 async fn load_artifact_registry(
896 &self,
897 binding_name: &str,
898 ) -> Result<Arc<dyn ArtifactRegistry>> {
899 if let Some(cached) = self
900 .get_cached::<Arc<dyn ArtifactRegistry>>("artifact_registry", binding_name)
901 .await
902 {
903 return Ok(cached);
904 }
905
906 use alien_core::bindings::ArtifactRegistryBinding;
907
908 let binding: ArtifactRegistryBinding =
909 self.parse_binding(binding_name, "artifact registry")?;
910
911 let registry: Arc<dyn ArtifactRegistry> = match binding {
912 #[cfg(feature = "aws")]
913 ArtifactRegistryBinding::Ecr { .. } => {
914 use crate::providers::artifact_registry::ecr::EcrArtifactRegistry;
915
916 let aws_config = self.client_config.aws_config().ok_or_else(|| {
917 AlienError::new(ErrorData::ClientConfigInvalid {
918 platform: Platform::Aws,
919 message: "AWS config not available".to_string(),
920 })
921 })?;
922 let credentials =
923 alien_aws_clients::AwsCredentialProvider::from_config(aws_config.clone())
924 .await
925 .context(ErrorData::ClientConfigInvalid {
926 platform: Platform::Aws,
927 message: "Failed to create AWS credential provider".to_string(),
928 })?;
929
930 let registry: Arc<dyn ArtifactRegistry> = Arc::new(
931 EcrArtifactRegistry::new(binding_name.to_string(), binding, &credentials)
932 .await
933 .context(ErrorData::config_invalid(
934 binding_name,
935 "Failed to initialize AWS ECR artifact registry",
936 ))?,
937 );
938 Ok(registry)
939 }
940 #[cfg(not(feature = "aws"))]
941 ArtifactRegistryBinding::Ecr { .. } => {
942 Err(AlienError::new(ErrorData::FeatureNotEnabled {
943 feature: "aws".to_string(),
944 }))
945 }
946
947 #[cfg(feature = "azure")]
948 ArtifactRegistryBinding::Acr { .. } => {
949 use crate::providers::artifact_registry::acr::AcrArtifactRegistry;
950
951 let azure_config = self.client_config.azure_config().ok_or_else(|| {
952 AlienError::new(ErrorData::ClientConfigInvalid {
953 platform: Platform::Azure,
954 message: "Azure config not available".to_string(),
955 })
956 })?;
957
958 let registry: Arc<dyn ArtifactRegistry> = Arc::new(
959 AcrArtifactRegistry::new(binding_name.to_string(), binding, azure_config)
960 .await
961 .context(ErrorData::config_invalid(
962 binding_name,
963 "Failed to initialize Azure ACR artifact registry",
964 ))?,
965 );
966 Ok(registry)
967 }
968 #[cfg(not(feature = "azure"))]
969 ArtifactRegistryBinding::Acr { .. } => {
970 Err(AlienError::new(ErrorData::FeatureNotEnabled {
971 feature: "azure".to_string(),
972 }))
973 }
974
975 #[cfg(feature = "gcp")]
976 ArtifactRegistryBinding::Gar { .. } => {
977 use crate::providers::artifact_registry::gar::GarArtifactRegistry;
978
979 let gcp_config = self.client_config.gcp_config().ok_or_else(|| {
980 AlienError::new(ErrorData::ClientConfigInvalid {
981 platform: Platform::Gcp,
982 message: "GCP config not available".to_string(),
983 })
984 })?;
985
986 let registry: Arc<dyn ArtifactRegistry> = Arc::new(
987 GarArtifactRegistry::new(binding_name.to_string(), binding, gcp_config)
988 .await
989 .context(ErrorData::config_invalid(
990 binding_name,
991 "Failed to initialize GCP GAR artifact registry",
992 ))?,
993 );
994 Ok(registry)
995 }
996 #[cfg(not(feature = "gcp"))]
997 ArtifactRegistryBinding::Gar { .. } => {
998 Err(AlienError::new(ErrorData::FeatureNotEnabled {
999 feature: "gcp".to_string(),
1000 }))
1001 }
1002
1003 #[cfg(feature = "local")]
1004 ArtifactRegistryBinding::Local { .. } => {
1005 use crate::providers::artifact_registry::local::LocalArtifactRegistry;
1006
1007 let registry: Arc<dyn ArtifactRegistry> = Arc::new(
1008 LocalArtifactRegistry::new(binding_name.to_string(), binding.clone()).await?,
1009 );
1010 Ok(registry)
1011 }
1012 #[cfg(not(feature = "local"))]
1013 ArtifactRegistryBinding::Local { .. } => {
1014 Err(AlienError::new(ErrorData::FeatureNotEnabled {
1015 feature: "local".to_string(),
1016 }))
1017 }
1018 }?;
1019
1020 self.put_cache("artifact_registry", binding_name, registry.clone())
1021 .await;
1022 Ok(registry)
1023 }
1024
1025 async fn load_vault(&self, binding_name: &str) -> Result<Arc<dyn Vault>> {
1026 if let Some(cached) = self
1027 .get_cached::<Arc<dyn Vault>>("vault", binding_name)
1028 .await
1029 {
1030 return Ok(cached);
1031 }
1032
1033 use alien_core::bindings::VaultBinding;
1034
1035 let binding: VaultBinding = self.parse_binding(binding_name, "vault")?;
1036
1037 let result: Arc<dyn Vault> = match binding {
1038 #[cfg(feature = "aws")]
1039 VaultBinding::ParameterStore(config) => {
1040 use crate::providers::vault::aws_parameter_store::AwsParameterStoreVault;
1041 use alien_aws_clients::ssm::SsmClient;
1042
1043 let aws_config = self.client_config.aws_config().ok_or_else(|| {
1044 AlienError::new(ErrorData::ClientConfigInvalid {
1045 platform: Platform::Aws,
1046 message: "AWS config not available".to_string(),
1047 })
1048 })?;
1049 let credentials =
1050 alien_aws_clients::AwsCredentialProvider::from_config(aws_config.clone())
1051 .await
1052 .context(ErrorData::ClientConfigInvalid {
1053 platform: Platform::Aws,
1054 message: "Failed to create AWS credential provider".to_string(),
1055 })?;
1056
1057 let client = Arc::new(SsmClient::new(
1058 crate::http_client::create_http_client(),
1059 credentials,
1060 ));
1061
1062 let vault_prefix = config
1064 .vault_prefix
1065 .into_value(&binding_name, "vault_prefix")
1066 .context(ErrorData::config_invalid(
1067 binding_name,
1068 "Failed to extract vault_prefix from ParameterStore binding",
1069 ))?;
1070
1071 let vault: Arc<dyn Vault> =
1072 Arc::new(AwsParameterStoreVault::new(client, vault_prefix));
1073 Ok(vault)
1074 }
1075 #[cfg(not(feature = "aws"))]
1076 VaultBinding::ParameterStore(_) => Err(AlienError::new(ErrorData::FeatureNotEnabled {
1077 feature: "aws".to_string(),
1078 })),
1079
1080 #[cfg(feature = "azure")]
1081 VaultBinding::KeyVault(config) => {
1082 use crate::providers::vault::azure_key_vault::AzureKeyVault;
1083 use alien_azure_clients::keyvault::AzureKeyVaultSecretsClient;
1084 use alien_azure_clients::AzureTokenCache;
1085
1086 let azure_config = self.client_config.azure_config().ok_or_else(|| {
1087 AlienError::new(ErrorData::ClientConfigInvalid {
1088 platform: Platform::Azure,
1089 message: "Azure config not available".to_string(),
1090 })
1091 })?;
1092
1093 let client = Arc::new(AzureKeyVaultSecretsClient::new(
1094 crate::http_client::create_http_client(),
1095 AzureTokenCache::new(azure_config.clone()),
1096 ));
1097
1098 let vault_name = config
1100 .vault_name
1101 .into_value(&binding_name, "vault_name")
1102 .context(ErrorData::config_invalid(
1103 binding_name,
1104 "Failed to extract vault_name from KeyVault binding",
1105 ))?;
1106
1107 let vault_base_url = format!("https://{}.vault.azure.net", vault_name);
1110
1111 let vault: Arc<dyn Vault> = Arc::new(AzureKeyVault::new(client, vault_base_url));
1112 Ok(vault)
1113 }
1114 #[cfg(not(feature = "azure"))]
1115 VaultBinding::KeyVault(_) => Err(AlienError::new(ErrorData::FeatureNotEnabled {
1116 feature: "azure".to_string(),
1117 })),
1118
1119 #[cfg(feature = "gcp")]
1120 VaultBinding::SecretManager(config) => {
1121 use crate::providers::vault::gcp_secret_manager::GcpSecretManagerVault;
1122 use alien_gcp_clients::secret_manager::SecretManagerClient;
1123
1124 let gcp_config = self.client_config.gcp_config().ok_or_else(|| {
1125 AlienError::new(ErrorData::ClientConfigInvalid {
1126 platform: Platform::Gcp,
1127 message: "GCP config not available".to_string(),
1128 })
1129 })?;
1130
1131 let client = Arc::new(SecretManagerClient::new(
1132 crate::http_client::create_http_client(),
1133 gcp_config.clone(),
1134 ));
1135
1136 let vault_prefix = config
1138 .vault_prefix
1139 .into_value(&binding_name, "vault_prefix")
1140 .context(ErrorData::config_invalid(
1141 binding_name,
1142 "Failed to extract vault_prefix from SecretManager binding",
1143 ))?;
1144
1145 let vault: Arc<dyn Vault> = Arc::new(GcpSecretManagerVault::new(
1146 client,
1147 vault_prefix,
1148 gcp_config.project_id.clone(),
1149 ));
1150 Ok(vault)
1151 }
1152 #[cfg(not(feature = "gcp"))]
1153 VaultBinding::SecretManager(_) => Err(AlienError::new(ErrorData::FeatureNotEnabled {
1154 feature: "gcp".to_string(),
1155 })),
1156
1157 #[cfg(feature = "local")]
1158 VaultBinding::Local(config) => {
1159 use crate::providers::vault::local::LocalVault;
1160
1161 let vault_dir = config
1162 .data_dir
1163 .into_value(binding_name, "data_dir")
1164 .context(ErrorData::config_invalid(
1165 binding_name,
1166 "Failed to extract data_dir from vault binding",
1167 ))?;
1168
1169 let vault: Arc<dyn Vault> = Arc::new(LocalVault::new(
1170 binding_name.to_string(),
1171 std::path::PathBuf::from(vault_dir),
1172 ));
1173 Ok(vault)
1174 }
1175 #[cfg(not(feature = "local"))]
1176 VaultBinding::Local { .. } => Err(AlienError::new(ErrorData::FeatureNotEnabled {
1177 feature: "local".to_string(),
1178 })),
1179
1180 #[cfg(feature = "kubernetes")]
1181 VaultBinding::KubernetesSecret(config) => {
1182 use crate::providers::vault::kubernetes_secret::KubernetesSecretVault;
1183 use alien_k8s_clients::{secrets::SecretsApi, KubernetesClient};
1184
1185 let kubernetes_config =
1186 self.client_config.kubernetes_config().ok_or_else(|| {
1187 AlienError::new(ErrorData::ClientConfigInvalid {
1188 platform: Platform::Kubernetes,
1189 message: "Kubernetes config not available".to_string(),
1190 })
1191 })?;
1192
1193 let kubernetes_client = KubernetesClient::new(kubernetes_config.clone())
1194 .await
1195 .context(ErrorData::CloudPlatformError {
1196 message: "Failed to create Kubernetes client for vault".to_string(),
1197 resource_id: None,
1198 })?;
1199
1200 let client: Arc<dyn SecretsApi> = Arc::new(kubernetes_client);
1201
1202 let namespace = config
1204 .namespace
1205 .into_value(binding_name, "namespace")
1206 .context(ErrorData::config_invalid(
1207 binding_name,
1208 "Failed to extract namespace from KubernetesSecret binding",
1209 ))?;
1210
1211 let vault_prefix = config
1212 .vault_prefix
1213 .into_value(binding_name, "vault_prefix")
1214 .context(ErrorData::config_invalid(
1215 binding_name,
1216 "Failed to extract vault_prefix from KubernetesSecret binding",
1217 ))?;
1218
1219 let vault: Arc<dyn Vault> =
1220 Arc::new(KubernetesSecretVault::new(client, namespace, vault_prefix));
1221 Ok(vault)
1222 }
1223 #[cfg(not(feature = "kubernetes"))]
1224 VaultBinding::KubernetesSecret(_) => {
1225 Err(AlienError::new(ErrorData::FeatureNotEnabled {
1226 feature: "kubernetes".to_string(),
1227 }))
1228 }
1229 }?;
1230
1231 self.put_cache("vault", binding_name, result.clone()).await;
1232 Ok(result)
1233 }
1234
1235 async fn load_kv(&self, binding_name: &str) -> Result<Arc<dyn Kv>> {
1236 if let Some(cached) = self.get_cached::<Arc<dyn Kv>>("kv", binding_name).await {
1237 return Ok(cached);
1238 }
1239
1240 use alien_core::bindings::KvBinding;
1241
1242 let binding: KvBinding = self.parse_binding(binding_name, "KV")?;
1243
1244 let result: Arc<dyn Kv> = match binding {
1245 #[cfg(feature = "aws")]
1246 KvBinding::Dynamodb(config) => {
1247 use crate::providers::kv::aws_dynamodb::AwsDynamodbKv;
1248
1249 let table_name = config
1250 .table_name
1251 .into_value(binding_name, "table_name")
1252 .context(ErrorData::config_invalid(
1253 binding_name,
1254 "Failed to extract table_name from DynamoDB binding",
1255 ))?;
1256
1257 let aws_config = self.client_config.aws_config().ok_or_else(|| {
1258 AlienError::new(ErrorData::ClientConfigInvalid {
1259 platform: Platform::Aws,
1260 message: "AWS config not available".to_string(),
1261 })
1262 })?;
1263
1264 let credentials =
1265 alien_aws_clients::AwsCredentialProvider::from_config(aws_config.clone())
1266 .await
1267 .context(ErrorData::ClientConfigInvalid {
1268 platform: Platform::Aws,
1269 message: "Failed to create AWS credential provider".to_string(),
1270 })?;
1271 let dynamodb_client = alien_aws_clients::dynamodb::DynamoDbClient::new(
1272 crate::http_client::create_http_client(),
1273 credentials,
1274 );
1275 let kv_impl = AwsDynamodbKv::new(table_name, dynamodb_client);
1276 let kv: Arc<dyn Kv> = Arc::new(kv_impl);
1277 Ok(kv)
1278 }
1279 #[cfg(not(feature = "aws"))]
1280 KvBinding::Dynamodb(_) => Err(AlienError::new(ErrorData::FeatureNotEnabled {
1281 feature: "aws".to_string(),
1282 })),
1283
1284 #[cfg(feature = "gcp")]
1285 KvBinding::Firestore(config) => {
1286 use crate::providers::kv::gcp_firestore::GcpFirestoreKv;
1287 use alien_gcp_clients::firestore::FirestoreClient;
1288
1289 let gcp_config = self.client_config.gcp_config().ok_or_else(|| {
1290 AlienError::new(ErrorData::ClientConfigInvalid {
1291 platform: Platform::Gcp,
1292 message: "GCP config not available".to_string(),
1293 })
1294 })?;
1295
1296 let client = FirestoreClient::new(
1297 crate::http_client::create_http_client(),
1298 gcp_config.clone(),
1299 );
1300
1301 let project_id = config
1302 .project_id
1303 .into_value(binding_name, "project_id")
1304 .context(ErrorData::config_invalid(
1305 binding_name,
1306 "Failed to extract project_id from Firestore binding",
1307 ))?;
1308
1309 let database_id = config
1310 .database_id
1311 .into_value(binding_name, "database_id")
1312 .context(ErrorData::config_invalid(
1313 binding_name,
1314 "Failed to extract database_id from Firestore binding",
1315 ))?;
1316
1317 let collection_name = config
1318 .collection_name
1319 .into_value(binding_name, "collection_name")
1320 .context(ErrorData::config_invalid(
1321 binding_name,
1322 "Failed to extract collection_name from Firestore binding",
1323 ))?;
1324
1325 let kv: Arc<dyn Kv> = Arc::new(GcpFirestoreKv::new(
1326 client,
1327 project_id,
1328 database_id,
1329 collection_name,
1330 )?);
1331 Ok(kv)
1332 }
1333 #[cfg(not(feature = "gcp"))]
1334 KvBinding::Firestore(_) => Err(AlienError::new(ErrorData::FeatureNotEnabled {
1335 feature: "gcp".to_string(),
1336 })),
1337
1338 #[cfg(feature = "azure")]
1339 KvBinding::TableStorage(config) => {
1340 use crate::providers::kv::azure_table_storage::AzureTableStorageKv;
1341 use alien_azure_clients::tables::AzureTableStorageClient;
1342 use alien_azure_clients::AzureTokenCache;
1343
1344 let azure_config = self.client_config.azure_config().ok_or_else(|| {
1345 AlienError::new(ErrorData::ClientConfigInvalid {
1346 platform: Platform::Azure,
1347 message: "Azure config not available".to_string(),
1348 })
1349 })?;
1350
1351 let resource_group_name = config
1352 .resource_group_name
1353 .into_value(binding_name, "resource_group_name")
1354 .context(ErrorData::config_invalid(
1355 binding_name,
1356 "Failed to extract resource_group_name from TableStorage binding",
1357 ))?;
1358
1359 let account_name = config
1360 .account_name
1361 .into_value(binding_name, "account_name")
1362 .context(ErrorData::config_invalid(
1363 binding_name,
1364 "Failed to extract account_name from TableStorage binding",
1365 ))?;
1366
1367 let table_name = config
1368 .table_name
1369 .into_value(binding_name, "table_name")
1370 .context(ErrorData::config_invalid(
1371 binding_name,
1372 "Failed to extract table_name from TableStorage binding",
1373 ))?;
1374
1375 let client = AzureTableStorageClient::new(
1376 crate::http_client::create_http_client(),
1377 AzureTokenCache::new(azure_config.clone()),
1378 );
1379
1380 let kv_impl =
1381 AzureTableStorageKv::new(client, resource_group_name, account_name, table_name);
1382 let kv: Arc<dyn Kv> = Arc::new(kv_impl);
1383 Ok(kv)
1384 }
1385 #[cfg(not(feature = "azure"))]
1386 KvBinding::TableStorage(_) => Err(AlienError::new(ErrorData::FeatureNotEnabled {
1387 feature: "azure".to_string(),
1388 })),
1389
1390 #[cfg(feature = "local")]
1391 KvBinding::Local(local_binding) => {
1392 use crate::providers::kv::local::LocalKv;
1393 use std::path::PathBuf;
1394
1395 let data_dir = PathBuf::from(
1397 local_binding
1398 .data_dir
1399 .into_value(binding_name, "data_dir")
1400 .context(ErrorData::config_invalid(
1401 binding_name,
1402 "Failed to extract data_dir from Local binding",
1403 ))?,
1404 );
1405
1406 let kv_impl = LocalKv::new(data_dir).await?;
1408
1409 let kv: Arc<dyn Kv> = Arc::new(kv_impl);
1410 Ok(kv)
1411 }
1412 #[cfg(not(feature = "local"))]
1413 KvBinding::Local { .. } => Err(AlienError::new(ErrorData::FeatureNotEnabled {
1414 feature: "local".to_string(),
1415 })),
1416
1417 KvBinding::Redis(_) => Err(AlienError::new(ErrorData::UnsupportedBindingProvider {
1418 binding_name: binding_name.to_string(),
1419 env_var: binding_env_var(binding_name),
1420 provider: "redis".to_string(),
1421 })),
1422 }?;
1423
1424 self.put_cache("kv", binding_name, result.clone()).await;
1425 Ok(result)
1426 }
1427
1428 async fn load_postgres(&self, binding_name: &str) -> Result<Arc<dyn Postgres>> {
1429 let binding: PostgresBinding = self.parse_binding(binding_name, "Postgres")?;
1430 self.postgres.load(binding_name, &binding).await
1431 }
1432
1433 async fn load_queue(&self, binding_name: &str) -> Result<Arc<dyn Queue>> {
1434 if let Some(cached) = self
1435 .get_cached::<Arc<dyn Queue>>("queue", binding_name)
1436 .await
1437 {
1438 return Ok(cached);
1439 }
1440
1441 use alien_core::bindings::QueueBinding;
1442
1443 let binding: QueueBinding = self.parse_binding(binding_name, "Queue")?;
1444
1445 let result: Arc<dyn Queue> = match binding {
1446 #[cfg(feature = "aws")]
1447 QueueBinding::Sqs(config) => {
1448 use crate::providers::queue::aws_sqs::AwsSqsQueue;
1449
1450 let queue_url = config
1451 .queue_url
1452 .into_value(binding_name, "queue_url")
1453 .context(ErrorData::config_invalid(
1454 binding_name,
1455 "Failed to extract queue_url from SQS binding",
1456 ))?;
1457
1458 let aws_config = self.client_config.aws_config().ok_or_else(|| {
1459 AlienError::new(ErrorData::ClientConfigInvalid {
1460 platform: Platform::Aws,
1461 message: "AWS config not available".to_string(),
1462 })
1463 })?;
1464 let credentials =
1465 alien_aws_clients::AwsCredentialProvider::from_config(aws_config.clone())
1466 .await
1467 .context(ErrorData::ClientConfigInvalid {
1468 platform: Platform::Aws,
1469 message: "Failed to create AWS credential provider".to_string(),
1470 })?;
1471 let client = alien_aws_clients::sqs::SqsClient::new(
1472 crate::http_client::create_http_client(),
1473 credentials,
1474 );
1475 let q: Arc<dyn Queue> = Arc::new(AwsSqsQueue::new(queue_url, client));
1476 Ok(q)
1477 }
1478 #[cfg(not(feature = "aws"))]
1479 QueueBinding::Sqs(_) => Err(AlienError::new(ErrorData::FeatureNotEnabled {
1480 feature: "aws".to_string(),
1481 })),
1482
1483 #[cfg(feature = "gcp")]
1484 QueueBinding::Pubsub(config) => {
1485 use crate::providers::queue::gcp_pubsub::GcpPubSubQueue;
1486 let topic_name = config.topic.into_value(binding_name, "topic").context(
1487 ErrorData::config_invalid(binding_name, "Failed to extract topic"),
1488 )?;
1489 let subscription_name = config
1490 .subscription
1491 .into_value(binding_name, "subscription")
1492 .context(ErrorData::config_invalid(
1493 binding_name,
1494 "Failed to extract subscription",
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
1503 let topic = if let Some(short) =
1505 topic_name.strip_prefix(&format!("projects/{}/topics/", gcp_config.project_id))
1506 {
1507 short.to_string()
1508 } else {
1509 topic_name
1510 };
1511 let subscription = if let Some(short) = subscription_name.strip_prefix(&format!(
1512 "projects/{}/subscriptions/",
1513 gcp_config.project_id
1514 )) {
1515 short.to_string()
1516 } else {
1517 subscription_name
1518 };
1519
1520 let q: Arc<dyn Queue> =
1521 Arc::new(GcpPubSubQueue::new(topic, subscription, gcp_config.clone()).await?);
1522 Ok(q)
1523 }
1524 #[cfg(not(feature = "gcp"))]
1525 QueueBinding::Pubsub(_) => Err(AlienError::new(ErrorData::FeatureNotEnabled {
1526 feature: "gcp".to_string(),
1527 })),
1528
1529 #[cfg(feature = "azure")]
1530 QueueBinding::Servicebus(config) => {
1531 use crate::providers::queue::azure_service_bus::AzureServiceBusQueue;
1532 let namespace = config
1533 .namespace
1534 .into_value(binding_name, "namespace")
1535 .context(ErrorData::config_invalid(
1536 binding_name,
1537 "Failed to extract namespace",
1538 ))?;
1539 let queue_name = config
1540 .queue_name
1541 .into_value(binding_name, "queue_name")
1542 .context(ErrorData::config_invalid(
1543 binding_name,
1544 "Failed to extract queue_name",
1545 ))?;
1546 let azure_config = self.client_config.azure_config().ok_or_else(|| {
1547 AlienError::new(ErrorData::ClientConfigInvalid {
1548 platform: Platform::Azure,
1549 message: "Azure config not available".to_string(),
1550 })
1551 })?;
1552 let q: Arc<dyn Queue> = Arc::new(
1553 AzureServiceBusQueue::new(namespace, queue_name, azure_config.clone()).await?,
1554 );
1555 Ok(q)
1556 }
1557 #[cfg(not(feature = "azure"))]
1558 QueueBinding::Servicebus(_) => Err(AlienError::new(ErrorData::FeatureNotEnabled {
1559 feature: "azure".to_string(),
1560 })),
1561
1562 #[cfg(feature = "local")]
1563 QueueBinding::Local(config) => {
1564 use crate::providers::queue::local::LocalQueue;
1565
1566 let queue = LocalQueue::from_binding(config).await?;
1567 let q: Arc<dyn Queue> = Arc::new(queue);
1568 Ok(q)
1569 }
1570 #[cfg(not(feature = "local"))]
1571 QueueBinding::Local(_) => Err(AlienError::new(ErrorData::FeatureNotEnabled {
1572 feature: "local".to_string(),
1573 })),
1574 }?;
1575
1576 self.put_cache("queue", binding_name, result.clone()).await;
1577 Ok(result)
1578 }
1579
1580 async fn load_worker(&self, binding_name: &str) -> Result<Arc<dyn Worker>> {
1581 use alien_core::bindings::WorkerBinding;
1582
1583 let binding: WorkerBinding = self.parse_binding(binding_name, "worker")?;
1584
1585 match binding {
1586 #[cfg(feature = "aws")]
1587 WorkerBinding::Lambda(lambda_binding) => {
1588 use crate::providers::worker::LambdaWorker;
1589
1590 let aws_config = self.client_config.aws_config().ok_or_else(|| {
1591 AlienError::new(ErrorData::ClientConfigInvalid {
1592 platform: Platform::Aws,
1593 message: "AWS config not available".to_string(),
1594 })
1595 })?;
1596 let credentials =
1597 alien_aws_clients::AwsCredentialProvider::from_config(aws_config.clone())
1598 .await
1599 .context(ErrorData::ClientConfigInvalid {
1600 platform: Platform::Aws,
1601 message: "Failed to create AWS credential provider".to_string(),
1602 })?;
1603 let client = crate::http_client::create_http_client();
1604
1605 let function_impl = LambdaWorker::new(client, credentials, lambda_binding);
1606 let function: Arc<dyn Worker> = Arc::new(function_impl);
1607 Ok(function)
1608 }
1609 #[cfg(not(feature = "aws"))]
1610 WorkerBinding::Lambda(_) => Err(AlienError::new(ErrorData::FeatureNotEnabled {
1611 feature: "aws".to_string(),
1612 })),
1613
1614 #[cfg(feature = "gcp")]
1615 WorkerBinding::CloudRun(cloudrun_binding) => {
1616 use crate::providers::worker::CloudRunWorker;
1617
1618 let gcp_config = self.client_config.gcp_config().ok_or_else(|| {
1619 AlienError::new(ErrorData::ClientConfigInvalid {
1620 platform: Platform::Gcp,
1621 message: "GCP config not available".to_string(),
1622 })
1623 })?;
1624 let client = crate::http_client::create_http_client();
1625
1626 let function_impl =
1627 CloudRunWorker::new(client, gcp_config.clone(), cloudrun_binding);
1628 let function: Arc<dyn Worker> = Arc::new(function_impl);
1629 Ok(function)
1630 }
1631 #[cfg(not(feature = "gcp"))]
1632 WorkerBinding::CloudRun(_) => Err(AlienError::new(ErrorData::FeatureNotEnabled {
1633 feature: "gcp".to_string(),
1634 })),
1635
1636 #[cfg(feature = "azure")]
1637 WorkerBinding::ContainerApp(container_app_binding) => {
1638 use crate::providers::worker::ContainerAppWorker;
1639
1640 let azure_config = self.client_config.azure_config().ok_or_else(|| {
1641 AlienError::new(ErrorData::ClientConfigInvalid {
1642 platform: Platform::Azure,
1643 message: "Azure config not available".to_string(),
1644 })
1645 })?;
1646 let client = crate::http_client::create_http_client();
1647
1648 let function_impl =
1649 ContainerAppWorker::new(client, azure_config.clone(), container_app_binding);
1650 let function: Arc<dyn Worker> = Arc::new(function_impl);
1651 Ok(function)
1652 }
1653 #[cfg(not(feature = "azure"))]
1654 WorkerBinding::ContainerApp(_) => Err(AlienError::new(ErrorData::FeatureNotEnabled {
1655 feature: "azure".to_string(),
1656 })),
1657
1658 #[cfg(feature = "local")]
1659 WorkerBinding::Local(local_binding) => {
1660 use crate::providers::worker::LocalWorker;
1661
1662 let function_impl = LocalWorker::new(local_binding);
1663 let function: Arc<dyn Worker> = Arc::new(function_impl);
1664 Ok(function)
1665 }
1666 #[cfg(not(feature = "local"))]
1667 WorkerBinding::Local(_) => Err(AlienError::new(ErrorData::FeatureNotEnabled {
1668 feature: "local".to_string(),
1669 })),
1670
1671 #[cfg(feature = "kubernetes")]
1672 WorkerBinding::Kubernetes(kubernetes_binding) => {
1673 use crate::providers::worker::KubernetesWorker;
1674
1675 let function_impl =
1676 KubernetesWorker::new(binding_name.to_string(), kubernetes_binding)?;
1677 let function: Arc<dyn Worker> = Arc::new(function_impl);
1678 Ok(function)
1679 }
1680 #[cfg(not(feature = "kubernetes"))]
1681 WorkerBinding::Kubernetes(_) => Err(AlienError::new(ErrorData::FeatureNotEnabled {
1682 feature: "kubernetes".to_string(),
1683 })),
1684 }
1685 }
1686
1687 async fn load_container(
1688 &self,
1689 binding_name: &str,
1690 ) -> Result<Arc<dyn crate::traits::Container>> {
1691 use alien_core::bindings::ContainerBinding;
1692
1693 let binding: ContainerBinding = self.parse_binding(binding_name, "container")?;
1694
1695 match binding {
1696 ContainerBinding::Horizon(horizon_binding) => {
1697 use crate::providers::container::HorizonContainer;
1698
1699 let container_impl = HorizonContainer::new(horizon_binding)?;
1700 let container: Arc<dyn crate::traits::Container> = Arc::new(container_impl);
1701 Ok(container)
1702 }
1703
1704 #[cfg(feature = "local")]
1705 ContainerBinding::Local(local_binding) => {
1706 use crate::providers::container::LocalContainer;
1707
1708 let container_impl = LocalContainer::new(local_binding)?;
1709 let container: Arc<dyn crate::traits::Container> = Arc::new(container_impl);
1710 Ok(container)
1711 }
1712 #[cfg(not(feature = "local"))]
1713 ContainerBinding::Local(_) => Err(AlienError::new(ErrorData::FeatureNotEnabled {
1714 feature: "local".to_string(),
1715 })),
1716
1717 #[cfg(feature = "kubernetes")]
1718 ContainerBinding::Kubernetes(kubernetes_binding) => {
1719 use crate::providers::container::KubernetesContainer;
1720
1721 let container_impl =
1722 KubernetesContainer::new(binding_name.to_string(), kubernetes_binding)?;
1723 let container: Arc<dyn crate::traits::Container> = Arc::new(container_impl);
1724 Ok(container)
1725 }
1726 #[cfg(not(feature = "kubernetes"))]
1727 ContainerBinding::Kubernetes(_) => Err(AlienError::new(ErrorData::FeatureNotEnabled {
1728 feature: "kubernetes".to_string(),
1729 })),
1730 }
1731 }
1732
1733 async fn load_service_account(
1734 &self,
1735 binding_name: &str,
1736 ) -> Result<Arc<dyn crate::traits::ServiceAccount>> {
1737 use alien_core::bindings::ServiceAccountBinding;
1738
1739 let binding: ServiceAccountBinding = self.parse_binding(binding_name, "service account")?;
1740
1741 match binding {
1742 #[cfg(feature = "aws")]
1743 ServiceAccountBinding::AwsIam(aws_binding) => {
1744 use crate::providers::service_account::aws_iam::AwsIamServiceAccount;
1745
1746 let aws_config = self.client_config.aws_config().ok_or_else(|| {
1747 AlienError::new(ErrorData::ClientConfigInvalid {
1748 platform: Platform::Aws,
1749 message: "AWS config not available".to_string(),
1750 })
1751 })?;
1752 let client = crate::http_client::create_http_client();
1753
1754 let service_account_impl =
1755 AwsIamServiceAccount::new(client, aws_config.clone(), aws_binding);
1756 let service_account: Arc<dyn crate::traits::ServiceAccount> =
1757 Arc::new(service_account_impl);
1758 Ok(service_account)
1759 }
1760 #[cfg(not(feature = "aws"))]
1761 ServiceAccountBinding::AwsIam(_) => {
1762 Err(AlienError::new(ErrorData::FeatureNotEnabled {
1763 feature: "aws".to_string(),
1764 }))
1765 }
1766
1767 #[cfg(feature = "gcp")]
1768 ServiceAccountBinding::GcpServiceAccount(gcp_binding) => {
1769 use crate::providers::service_account::gcp_service_account::GcpServiceAccount;
1770
1771 let gcp_config = self.client_config.gcp_config().ok_or_else(|| {
1772 AlienError::new(ErrorData::ClientConfigInvalid {
1773 platform: Platform::Gcp,
1774 message: "GCP config not available".to_string(),
1775 })
1776 })?;
1777 let client = crate::http_client::create_http_client();
1778
1779 let service_account_impl =
1780 GcpServiceAccount::new(client, gcp_config.clone(), gcp_binding);
1781 let service_account: Arc<dyn crate::traits::ServiceAccount> =
1782 Arc::new(service_account_impl);
1783 Ok(service_account)
1784 }
1785 #[cfg(not(feature = "gcp"))]
1786 ServiceAccountBinding::GcpServiceAccount(_) => {
1787 Err(AlienError::new(ErrorData::FeatureNotEnabled {
1788 feature: "gcp".to_string(),
1789 }))
1790 }
1791
1792 #[cfg(feature = "azure")]
1793 ServiceAccountBinding::AzureManagedIdentity(azure_binding) => {
1794 use crate::providers::service_account::azure_managed_identity::AzureManagedIdentityServiceAccount;
1795
1796 let azure_config = self.client_config.azure_config().ok_or_else(|| {
1797 AlienError::new(ErrorData::ClientConfigInvalid {
1798 platform: Platform::Azure,
1799 message: "Azure config not available".to_string(),
1800 })
1801 })?;
1802
1803 let service_account_impl =
1804 AzureManagedIdentityServiceAccount::new(azure_config.clone(), azure_binding);
1805 let service_account: Arc<dyn crate::traits::ServiceAccount> =
1806 Arc::new(service_account_impl);
1807 Ok(service_account)
1808 }
1809 #[cfg(not(feature = "azure"))]
1810 ServiceAccountBinding::AzureManagedIdentity(_) => {
1811 Err(AlienError::new(ErrorData::FeatureNotEnabled {
1812 feature: "azure".to_string(),
1813 }))
1814 }
1815 }
1816 }
1817
1818 async fn load_sandbox(&self, binding_name: &str) -> Result<Arc<dyn crate::traits::Sandbox>> {
1819 use alien_core::bindings::SandboxBinding;
1820
1821 let binding: SandboxBinding = self.parse_binding(binding_name, "sandbox")?;
1824
1825 match binding {
1826 #[cfg(feature = "local")]
1827 SandboxBinding::Local(local_binding) => {
1828 use crate::providers::sandbox::local::LocalSandbox;
1829
1830 let sandbox: Arc<dyn crate::traits::Sandbox> =
1831 Arc::new(LocalSandbox::new(binding_name, &local_binding).await?);
1832 Ok(sandbox)
1833 }
1834 #[cfg(feature = "kubernetes")]
1835 SandboxBinding::Kubernetes(kubernetes_binding) => {
1836 use crate::providers::sandbox::kubernetes::KubernetesSandbox;
1837
1838 let sandbox: Arc<dyn crate::traits::Sandbox> = Arc::new(KubernetesSandbox::new(
1839 binding_name,
1840 &kubernetes_binding,
1841 binding_name,
1842 )?);
1843 Ok(sandbox)
1844 }
1845 #[cfg(feature = "gcp")]
1846 SandboxBinding::Gcp(gcp_binding) => {
1847 use crate::providers::sandbox::gcp::GcpSandbox;
1848
1849 let sandbox: Arc<dyn crate::traits::Sandbox> =
1850 Arc::new(GcpSandbox::new(binding_name, &gcp_binding)?);
1851 Ok(sandbox)
1852 }
1853 #[cfg(feature = "azure")]
1854 SandboxBinding::Azure(azure_binding) => {
1855 use crate::providers::sandbox::azure::AzureSandbox;
1856 use alien_azure_clients::azure::sandbox_data_plane::AzureSandboxDataPlaneClient;
1857 use alien_azure_clients::azure::token_cache::AzureTokenCache;
1858
1859 let azure_config = self.client_config.azure_config().ok_or_else(|| {
1860 AlienError::new(ErrorData::ClientConfigInvalid {
1861 platform: Platform::Azure,
1862 message: "Azure config not available".to_string(),
1863 })
1864 })?;
1865
1866 let invalid = |field: &str| {
1867 AlienError::new(ErrorData::BindingConfigInvalid {
1868 binding_name: binding_name.to_string(),
1869 env_var: alien_core::bindings::binding_env_var_name(binding_name),
1870 reason: format!("sandbox binding field '{field}' is not a concrete value"),
1871 })
1872 };
1873
1874 let group = azure_binding
1875 .sandbox_group
1876 .into_value(binding_name, "sandboxGroup")
1877 .map_err(|_| invalid("sandboxGroup"))?;
1878 let region = azure_binding
1879 .region
1880 .into_value(binding_name, "region")
1881 .map_err(|_| invalid("region"))?;
1882 let resource_group = azure_binding
1883 .resource_group
1884 .into_value(binding_name, "resourceGroup")
1885 .map_err(|_| invalid("resourceGroup"))?;
1886
1887 let client = AzureSandboxDataPlaneClient::new(
1888 reqwest::Client::new(),
1889 ®ion,
1890 &resource_group,
1891 AzureTokenCache::new(azure_config.clone()),
1892 );
1893
1894 let sandbox: Arc<dyn crate::traits::Sandbox> = Arc::new(AzureSandbox::new(
1897 Arc::new(client),
1898 group,
1899 "ubuntu".to_string(),
1900 "1000m".to_string(),
1901 "2048Mi".to_string(),
1902 ));
1903 Ok(sandbox)
1904 }
1905 #[cfg(feature = "aws")]
1906 SandboxBinding::Aws(aws_binding) => {
1907 use crate::providers::sandbox::aws::AwsSandbox;
1908 use alien_aws_clients::aws::lambda_microvms::LambdaMicrovmsClient;
1909
1910 let aws_config = self.client_config.aws_config().ok_or_else(|| {
1911 AlienError::new(ErrorData::ClientConfigInvalid {
1912 platform: Platform::Aws,
1913 message: "AWS config not available".to_string(),
1914 })
1915 })?;
1916
1917 let invalid = |field: &str| {
1918 AlienError::new(ErrorData::BindingConfigInvalid {
1919 binding_name: binding_name.to_string(),
1920 env_var: alien_core::bindings::binding_env_var_name(binding_name),
1921 reason: format!("sandbox binding field '{field}' is not a concrete value"),
1922 })
1923 };
1924
1925 let image_arn = aws_binding
1926 .image_arn
1927 .into_value(binding_name, "imageArn")
1928 .map_err(|_| invalid("imageArn"))?;
1929 let image_version = aws_binding
1930 .image_version
1931 .into_value(binding_name, "imageVersion")
1932 .map_err(|_| invalid("imageVersion"))?;
1933 let region = aws_binding
1934 .region
1935 .into_value(binding_name, "region")
1936 .map_err(|_| invalid("region"))?;
1937 if aws_binding.execution_role_arn.is_some() {
1938 return Err(AlienError::new(ErrorData::BindingConfigInvalid {
1943 binding_name: binding_name.to_string(),
1944 env_var: alien_core::bindings::binding_env_var_name(binding_name),
1945 reason: "sandbox binding field 'executionRoleArn' is set; a session can \
1946 read that role's credentials from instance metadata, which the \
1947 egress connector does not reach"
1948 .to_string(),
1949 }));
1950 }
1951
1952 let mut config = aws_config.clone();
1955 config.region = region;
1956
1957 let credentials = alien_aws_clients::AwsCredentialProvider::from_config(config)
1958 .await
1959 .context(ErrorData::BindingSetupFailed {
1960 binding_type: "AWS sandbox".to_string(),
1961 reason: "Failed to create credential provider".to_string(),
1962 })?;
1963
1964 let client = LambdaMicrovmsClient::new(reqwest::Client::new(), credentials);
1965
1966 let egress_connector_arns = aws_binding
1967 .egress_connector_arns
1968 .into_iter()
1969 .map(|value| {
1970 value
1971 .into_value(binding_name, "egressConnectorArns")
1972 .map_err(|_| invalid("egressConnectorArns"))
1973 })
1974 .collect::<Result<Vec<_>>>()?;
1975 if egress_connector_arns.is_empty() != aws_binding.allow_egress {
1979 let reason = if aws_binding.allow_egress {
1980 "sandbox binding declares open egress and also names egress connectors; \
1981 a session cannot be both open and routed through a denying connector"
1982 } else {
1983 "sandbox binding field 'egressConnectorArns' is empty; a MicroVM started \
1984 with no egress connector reaches the public internet"
1985 };
1986 return Err(AlienError::new(ErrorData::BindingConfigInvalid {
1987 binding_name: binding_name.to_string(),
1988 env_var: alien_core::bindings::binding_env_var_name(binding_name),
1989 reason: reason.to_string(),
1990 }));
1991 }
1992
1993 let sandbox: Arc<dyn crate::traits::Sandbox> = Arc::new(AwsSandbox::new(
1994 Arc::new(client),
1995 image_arn,
1996 image_version,
1997 egress_connector_arns,
1998 aws_binding.preview_ports,
1999 aws_binding.idle_suspend_seconds,
2000 aws_binding.max_lifetime_seconds,
2001 ));
2002 Ok(sandbox)
2003 }
2004 #[cfg(not(feature = "aws"))]
2007 SandboxBinding::Aws(_) => Err(not_built("aws")),
2008 #[cfg(not(feature = "azure"))]
2009 SandboxBinding::Azure(_) => Err(not_built("azure")),
2010 #[cfg(not(feature = "gcp"))]
2011 SandboxBinding::Gcp(_) => Err(not_built("gcp")),
2012 #[cfg(not(feature = "kubernetes"))]
2013 SandboxBinding::Kubernetes(_) => Err(not_built("kubernetes")),
2014 #[cfg(not(feature = "local"))]
2015 SandboxBinding::Local(_) => Err(not_built("local")),
2016 }
2017 }
2018}
2019
2020#[allow(dead_code)]
2024fn not_built(backend: &str) -> AlienError<ErrorData> {
2028 AlienError::new(ErrorData::OperationNotSupported {
2029 operation: format!("load_sandbox({backend})"),
2030 reason: "this build does not include the sandbox backend for that platform".to_string(),
2031 })
2032}
2033
2034#[cfg(test)]
2035mod tests {
2036 use super::*;
2037 use alien_core::ENV_ALIEN_DEPLOYMENT_TYPE;
2038
2039 fn kubernetes_aws_env() -> HashMap<String, String> {
2040 HashMap::from([
2041 (
2042 ENV_ALIEN_DEPLOYMENT_TYPE.to_string(),
2043 Platform::Kubernetes.as_str().to_string(),
2044 ),
2045 (
2046 ENV_OPERATOR_BASE_PLATFORM.to_string(),
2047 Platform::Aws.as_str().to_string(),
2048 ),
2049 (
2050 "KUBERNETES_SERVICE_HOST".to_string(),
2051 "10.0.0.1".to_string(),
2052 ),
2053 ("KUBERNETES_SERVICE_PORT".to_string(), "443".to_string()),
2054 ("AWS_REGION".to_string(), "us-east-1".to_string()),
2055 ("AWS_ACCOUNT_ID".to_string(), "123456789012".to_string()),
2056 ("AWS_ACCESS_KEY_ID".to_string(), "test".to_string()),
2057 ("AWS_SECRET_ACCESS_KEY".to_string(), "test".to_string()),
2058 ])
2059 }
2060
2061 #[cfg(feature = "kubernetes")]
2062 fn kubernetes_azure_env() -> HashMap<String, String> {
2063 HashMap::from([
2064 (
2065 ENV_ALIEN_DEPLOYMENT_TYPE.to_string(),
2066 Platform::Kubernetes.as_str().to_string(),
2067 ),
2068 (
2069 ENV_OPERATOR_BASE_PLATFORM.to_string(),
2070 Platform::Azure.as_str().to_string(),
2071 ),
2072 (
2073 "KUBERNETES_SERVICE_HOST".to_string(),
2074 "10.0.0.1".to_string(),
2075 ),
2076 ("KUBERNETES_SERVICE_PORT".to_string(), "443".to_string()),
2077 (
2078 "AZURE_SUBSCRIPTION_ID".to_string(),
2079 "00000000-0000-0000-0000-000000000000".to_string(),
2080 ),
2081 (
2082 "AZURE_TENANT_ID".to_string(),
2083 "11111111-1111-1111-1111-111111111111".to_string(),
2084 ),
2085 ("AZURE_REGION".to_string(), "eastus".to_string()),
2086 (
2087 "AZURE_CLIENT_ID".to_string(),
2088 "22222222-2222-2222-2222-222222222222".to_string(),
2089 ),
2090 (
2091 "AZURE_FEDERATED_TOKEN_FILE".to_string(),
2092 "/var/run/secrets/azure/tokens/azure-identity-token".to_string(),
2093 ),
2094 (
2095 "AZURE_AUTHORITY_HOST".to_string(),
2096 "https://login.microsoftonline.com/".to_string(),
2097 ),
2098 ])
2099 }
2100
2101 #[tokio::test]
2102 async fn lazy_env_provider_defers_cloud_client_config_until_binding_use() {
2103 let env = HashMap::from([
2104 (
2105 ENV_ALIEN_DEPLOYMENT_TYPE.to_string(),
2106 Platform::Aws.as_str().to_string(),
2107 ),
2108 ("AWS_EC2_METADATA_DISABLED".to_string(), "true".to_string()),
2109 (
2110 "AWS_PROFILE".to_string(),
2111 "__alien_missing_test_profile__".to_string(),
2112 ),
2113 (
2114 "ALIEN_SECRETS_BINDING".to_string(),
2115 r#"{"service":"parameter-store","vaultPrefix":"test-secrets"}"#.to_string(),
2116 ),
2117 ]);
2118
2119 let provider = BindingsProvider::from_env_lazy(env)
2120 .expect("lazy provider construction should validate binding JSON without AWS config");
2121
2122 let error = provider
2123 .load_vault("secrets")
2124 .await
2125 .expect_err("binding use should still require AWS client config");
2126
2127 assert_eq!(error.code, "CLIENT_CONFIG_INVALID");
2128 }
2129
2130 #[test]
2134 fn malformed_binding_json_fails_at_construction_for_both_lazy_constructors() {
2135 let env = HashMap::from([
2136 (
2137 ENV_ALIEN_DEPLOYMENT_TYPE.to_string(),
2138 Platform::Aws.as_str().to_string(),
2139 ),
2140 ("ALIEN_FILES_BINDING".to_string(), "not-json".to_string()),
2141 ]);
2142
2143 let error = BindingsProvider::from_env_lazy(env.clone())
2144 .expect_err("from_env_lazy must reject malformed binding JSON at construction");
2145 assert_eq!(error.code, "BINDING_CONFIG_INVALID");
2146
2147 let error = BindingsProvider::from_env_deferred(env)
2148 .expect_err("from_env_deferred must reject malformed binding JSON at construction");
2149 assert_eq!(error.code, "BINDING_CONFIG_INVALID");
2150 }
2151
2152 #[cfg(feature = "kubernetes")]
2155 #[tokio::test]
2156 async fn from_env_builds_kubernetes_cloud_config_when_base_platform_is_set() {
2157 let provider = BindingsProvider::from_env(kubernetes_aws_env())
2158 .await
2159 .unwrap();
2160
2161 assert!(provider.client_config.kubernetes_config().is_some());
2162 assert!(provider.client_config.aws_config().is_some());
2163 assert!(matches!(
2164 provider.client_config,
2165 ClientConfig::KubernetesCloud { .. }
2166 ));
2167 }
2168
2169 #[cfg(feature = "kubernetes")]
2170 #[tokio::test]
2171 async fn from_env_builds_kubernetes_cloud_config_for_azure_workload_identity() {
2172 let provider = BindingsProvider::from_env(kubernetes_azure_env())
2173 .await
2174 .unwrap();
2175
2176 assert!(provider.client_config.kubernetes_config().is_some());
2177 assert!(provider.client_config.azure_config().is_some());
2178 assert!(matches!(
2179 provider.client_config,
2180 ClientConfig::KubernetesCloud { .. }
2181 ));
2182 }
2183
2184 #[tokio::test]
2185 async fn from_env_rejects_non_cloud_kubernetes_base_platform() {
2186 let mut env = kubernetes_aws_env();
2187 env.insert(
2188 ENV_OPERATOR_BASE_PLATFORM.to_string(),
2189 Platform::Kubernetes.as_str().to_string(),
2190 );
2191
2192 let error = BindingsProvider::from_env(env).await.unwrap_err();
2193
2194 assert!(error.to_string().contains(ENV_OPERATOR_BASE_PLATFORM));
2195 }
2196
2197 #[tokio::test]
2198 async fn load_storage_for_unconfigured_binding_returns_binding_not_configured() {
2199 let env = HashMap::from([(
2200 ENV_ALIEN_DEPLOYMENT_TYPE.to_string(),
2201 Platform::Local.as_str().to_string(),
2202 )]);
2203 let provider = BindingsProvider::from_env(env)
2204 .await
2205 .expect("provider with no bindings configured should still construct");
2206
2207 let error = provider
2208 .load_storage("files")
2209 .await
2210 .expect_err("binding that was never configured should error");
2211
2212 assert_eq!(error.code, "BINDING_NOT_CONFIGURED");
2213 assert!(
2214 error.to_string().contains("ALIEN_FILES_BINDING"),
2215 "message should name the derived env var, got: {error}"
2216 );
2217 }
2218
2219 #[cfg(feature = "aws")]
2223 #[tokio::test]
2224 async fn a_sandbox_binding_whose_egress_fields_disagree_is_refused() {
2225 const CONNECTOR: &str = "arn:aws:lambda:us-east-1:123456789012:network-connector:nc-1";
2226
2227 async fn load(connectors: &str, allow_egress: bool) -> Result<()> {
2228 let env = HashMap::from([
2229 (
2230 ENV_ALIEN_DEPLOYMENT_TYPE.to_string(),
2231 Platform::Aws.as_str().to_string(),
2232 ),
2233 ("AWS_REGION".to_string(), "us-east-1".to_string()),
2234 ("AWS_ACCOUNT_ID".to_string(), "123456789012".to_string()),
2235 ("AWS_ACCESS_KEY_ID".to_string(), "test".to_string()),
2236 ("AWS_SECRET_ACCESS_KEY".to_string(), "test".to_string()),
2237 (
2238 "ALIEN_BOX_BINDING".to_string(),
2239 format!(
2240 r#"{{"service":"sandbox-aws",
2241 "imageArn":"arn:aws:lambda:us-east-1:123456789012:microvm-image:box",
2242 "imageVersion":"1.0",
2243 "region":"us-east-1",
2244 "allowEgress":{allow_egress},
2245 "egressConnectorArns":[{connectors}]}}"#
2246 ),
2247 ),
2248 ]);
2249 BindingsProvider::from_env(env)
2250 .await
2251 .expect("the binding JSON parses")
2252 .load_sandbox("box")
2253 .await
2254 .map(|_| ())
2255 }
2256
2257 let fail_open = load("", false)
2258 .await
2259 .expect_err("an empty connector list with allowEgress false is a fail-open default");
2260 assert_eq!(fail_open.code, "BINDING_CONFIG_INVALID");
2261 assert!(
2262 fail_open.to_string().contains("egressConnectorArns"),
2263 "the message should name the field that was refused, got: {fail_open}"
2264 );
2265
2266 let contradiction = load(&format!(r#""{CONNECTOR}""#), true)
2267 .await
2268 .expect_err("open egress and a denying connector cannot both be declared");
2269 assert_eq!(contradiction.code, "BINDING_CONFIG_INVALID");
2270
2271 load(&format!(r#""{CONNECTOR}""#), false)
2274 .await
2275 .expect("a deny binding names a connector and must load");
2276 load("", true)
2277 .await
2278 .expect("an allow binding names none and must load");
2279 }
2280
2281 #[cfg(feature = "aws")]
2285 #[tokio::test]
2286 async fn a_sandbox_binding_naming_an_execution_role_is_refused() {
2287 let env = HashMap::from([
2288 (
2289 ENV_ALIEN_DEPLOYMENT_TYPE.to_string(),
2290 Platform::Aws.as_str().to_string(),
2291 ),
2292 ("AWS_REGION".to_string(), "us-east-1".to_string()),
2293 ("AWS_ACCOUNT_ID".to_string(), "123456789012".to_string()),
2294 ("AWS_ACCESS_KEY_ID".to_string(), "test".to_string()),
2295 ("AWS_SECRET_ACCESS_KEY".to_string(), "test".to_string()),
2296 (
2297 "ALIEN_BOX_BINDING".to_string(),
2298 r#"{"service":"sandbox-aws",
2299 "imageArn":"arn:aws:lambda:us-east-1:123456789012:microvm-image:box",
2300 "imageVersion":"1.0",
2301 "region":"us-east-1",
2302 "executionRoleArn":"arn:aws:iam::123456789012:role/box",
2303 "egressConnectorArns":["arn:aws:lambda:us-east-1:123456789012:network-connector:nc-1"]}"#
2304 .to_string(),
2305 ),
2306 ]);
2307 let provider = BindingsProvider::from_env(env)
2308 .await
2309 .expect("provider construction only validates that the binding JSON parses");
2310
2311 let error = provider
2312 .load_sandbox("box")
2313 .await
2314 .expect_err("a sandbox binding naming an execution role should be refused");
2315
2316 assert_eq!(error.code, "BINDING_CONFIG_INVALID");
2317 assert!(
2318 error.to_string().contains("executionRoleArn"),
2319 "the message should name the field that was refused, got: {error}"
2320 );
2321 }
2322
2323 #[tokio::test]
2324 async fn load_kv_for_malformed_binding_json_returns_binding_config_invalid_with_env_var() {
2325 let env = HashMap::from([
2326 (
2327 ENV_ALIEN_DEPLOYMENT_TYPE.to_string(),
2328 Platform::Local.as_str().to_string(),
2329 ),
2330 (
2331 "ALIEN_CACHE_BINDING".to_string(),
2332 r#"{"service":"local-kv"}"#.to_string(), ),
2334 ]);
2335 let provider = BindingsProvider::from_env(env)
2336 .await
2337 .expect("provider construction only validates JSON parses, not field completeness");
2338
2339 let error = provider
2340 .load_kv("cache")
2341 .await
2342 .expect_err("binding missing a required field should error");
2343
2344 assert_eq!(error.code, "BINDING_CONFIG_INVALID");
2345 assert!(
2346 error.to_string().contains("ALIEN_CACHE_BINDING"),
2347 "message should name the env var, got: {error}"
2348 );
2349 }
2350
2351 mod selection {
2354 use super::*;
2355 use crate::traits::BindingsProviderApi;
2356 use alien_core::{
2357 ENV_ALIEN_DEPLOYMENT_ID, ENV_ALIEN_DEPLOYMENT_SERVICE_ACCOUNT,
2358 ENV_ALIEN_DEPLOYMENT_TOKEN, ENV_ALIEN_MANAGER_URL, ENV_ALIEN_RESOURCE_ID,
2359 };
2360 use axum::{extract::State, routing::post, Json, Router};
2361 use std::net::SocketAddr;
2362 use std::sync::atomic::{AtomicUsize, Ordering};
2363 use tempfile::TempDir;
2364
2365 async fn mint_handler(State(calls): State<Arc<AtomicUsize>>) -> Json<serde_json::Value> {
2367 calls.fetch_add(1, Ordering::SeqCst);
2368 let expires_at = (chrono::Utc::now() + chrono::Duration::seconds(3600)).to_rfc3339();
2369 Json(serde_json::json!({
2370 "clientConfig": { "platform": "local", "state_directory": "/tmp/alien-sel-test" },
2371 "expiresAt": expires_at,
2372 "principal": "local:mint-test",
2373 }))
2374 }
2375
2376 async fn spawn_mint_server() -> (String, Arc<AtomicUsize>) {
2377 let calls = Arc::new(AtomicUsize::new(0));
2378 let app = Router::new()
2379 .route("/v1/credentials/mint", post(mint_handler))
2380 .with_state(calls.clone());
2381 let listener = tokio::net::TcpListener::bind(SocketAddr::from(([127, 0, 0, 1], 0)))
2382 .await
2383 .expect("bind");
2384 let addr = listener.local_addr().expect("addr");
2385 tokio::spawn(async move {
2386 axum::serve(listener, app).await.expect("serve");
2387 });
2388 (format!("http://{addr}"), calls)
2389 }
2390
2391 fn local_storage_binding(dir: &TempDir) -> String {
2392 format!(
2393 r#"{{"service":"local-storage","storagePath":"{}"}}"#,
2394 dir.path().display()
2395 )
2396 }
2397
2398 fn mint_env(manager_url: &str) -> HashMap<String, String> {
2400 HashMap::from([
2401 (ENV_ALIEN_MANAGER_URL.to_string(), manager_url.to_string()),
2402 (
2403 ENV_ALIEN_DEPLOYMENT_TOKEN.to_string(),
2404 "ax_deploy_tok".to_string(),
2405 ),
2406 (ENV_ALIEN_DEPLOYMENT_ID.to_string(), "dep_1".to_string()),
2407 (
2408 ENV_ALIEN_DEPLOYMENT_SERVICE_ACCOUNT.to_string(),
2409 "management".to_string(),
2410 ),
2411 (ENV_ALIEN_RESOURCE_ID.to_string(), "api".to_string()),
2412 ])
2413 }
2414
2415 #[tokio::test]
2416 async fn native_config_wins_and_never_mints() {
2417 let (base_url, calls) = spawn_mint_server().await;
2421 let dir = TempDir::new().expect("tempdir");
2422
2423 let mut env = mint_env(&base_url);
2424 env.insert(
2425 ENV_ALIEN_DEPLOYMENT_TYPE.to_string(),
2426 Platform::Local.as_str().to_string(),
2427 );
2428 env.insert(
2429 "ALIEN_FILES_BINDING".to_string(),
2430 local_storage_binding(&dir),
2431 );
2432
2433 let provider = BindingsProvider::from_env_lazy(env).expect("lazy construct");
2434 provider
2435 .load_storage("files")
2436 .await
2437 .expect("native local storage should load");
2438
2439 assert_eq!(
2440 calls.load(Ordering::SeqCst),
2441 0,
2442 "native credentials must never trigger a mint"
2443 );
2444 }
2445
2446 #[tokio::test]
2447 async fn mints_when_native_config_unavailable() {
2448 let (base_url, calls) = spawn_mint_server().await;
2452 let dir = TempDir::new().expect("tempdir");
2453
2454 let mut env = mint_env(&base_url);
2455 env.insert(
2456 ENV_ALIEN_DEPLOYMENT_TYPE.to_string(),
2457 Platform::Aws.as_str().to_string(),
2458 );
2459 env.insert("AWS_EC2_METADATA_DISABLED".to_string(), "true".to_string());
2460 env.insert(
2461 "AWS_PROFILE".to_string(),
2462 "__alien_missing_test_profile__".to_string(),
2463 );
2464 env.insert(
2465 "ALIEN_FILES_BINDING".to_string(),
2466 local_storage_binding(&dir),
2467 );
2468
2469 let provider = BindingsProvider::from_env_lazy(env).expect("lazy construct");
2470 provider
2471 .load_storage("files")
2472 .await
2473 .expect("mint path should resolve a usable config");
2474
2475 assert_eq!(
2476 calls.load(Ordering::SeqCst),
2477 1,
2478 "unavailable native credentials must trigger exactly one mint"
2479 );
2480 }
2481
2482 #[tokio::test]
2483 async fn no_mint_contract_preserves_original_from_env_error() {
2484 let dir = TempDir::new().expect("tempdir");
2487 let env = HashMap::from([
2488 (
2489 ENV_ALIEN_DEPLOYMENT_TYPE.to_string(),
2490 Platform::Aws.as_str().to_string(),
2491 ),
2492 ("AWS_EC2_METADATA_DISABLED".to_string(), "true".to_string()),
2493 (
2494 "AWS_PROFILE".to_string(),
2495 "__alien_missing_test_profile__".to_string(),
2496 ),
2497 (
2498 "ALIEN_FILES_BINDING".to_string(),
2499 local_storage_binding(&dir),
2500 ),
2501 ]);
2502
2503 let provider = BindingsProvider::from_env_lazy(env).expect("lazy construct");
2504 let error = provider
2505 .load_storage("files")
2506 .await
2507 .expect_err("no creds and no mint contract must error");
2508
2509 assert_eq!(error.code, "CLIENT_CONFIG_INVALID");
2510 }
2511 }
2512}