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