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.max_lifetime_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 declared = |value: Option<alien_core::bindings::BindingValue<String>>,
1948 field: &'static str| {
1949 value
1950 .map(|value| value.into_value(binding_name, field))
1951 .transpose()
1952 .map_err(|_| invalid(field))
1953 };
1954 let cpu = declared(azure_binding.cpu, "cpu")?;
1955 let memory = declared(azure_binding.memory, "memory")?;
1956 let disk = declared(azure_binding.disk, "disk")?;
1957
1958 let sandbox: Arc<dyn crate::traits::Sandbox> = Arc::new(AzureSandbox::new(
1959 Arc::new(client),
1960 group,
1961 disk_image,
1962 azure_binding.egress,
1963 azure_binding.idle_pause_seconds,
1964 cpu.unwrap_or_else(|| DEFAULT_AZURE_CPU.to_string()),
1965 memory.unwrap_or_else(|| DEFAULT_AZURE_MEMORY.to_string()),
1966 disk,
1967 ));
1968 Ok(sandbox)
1969 }
1970 #[cfg(feature = "aws")]
1971 SandboxBinding::Aws(aws_binding) => {
1972 use crate::providers::sandbox::aws::AwsSandbox;
1973 use alien_aws_clients::aws::lambda_microvms::LambdaMicrovmsClient;
1974
1975 let aws_config = self.client_config.aws_config().ok_or_else(|| {
1976 AlienError::new(ErrorData::ClientConfigInvalid {
1977 platform: Platform::Aws,
1978 message: "AWS config not available".to_string(),
1979 })
1980 })?;
1981
1982 let invalid = |field: &str| {
1983 AlienError::new(ErrorData::BindingConfigInvalid {
1984 binding_name: binding_name.to_string(),
1985 env_var: alien_core::bindings::binding_env_var_name(binding_name),
1986 reason: format!("sandbox binding field '{field}' is not a concrete value"),
1987 })
1988 };
1989
1990 let image_arn = aws_binding
1991 .image_arn
1992 .into_value(binding_name, "imageArn")
1993 .map_err(|_| invalid("imageArn"))?;
1994 let image_version = aws_binding
1995 .image_version
1996 .into_value(binding_name, "imageVersion")
1997 .map_err(|_| invalid("imageVersion"))?;
1998 let region = aws_binding
1999 .region
2000 .into_value(binding_name, "region")
2001 .map_err(|_| invalid("region"))?;
2002 if aws_binding.execution_role_arn.is_some() {
2003 return Err(AlienError::new(ErrorData::BindingConfigInvalid {
2008 binding_name: binding_name.to_string(),
2009 env_var: alien_core::bindings::binding_env_var_name(binding_name),
2010 reason: "sandbox binding field 'executionRoleArn' is set; a sandbox can \
2011 read that role's credentials from instance metadata, which the \
2012 egress connector does not reach"
2013 .to_string(),
2014 }));
2015 }
2016
2017 let mut config = aws_config.clone();
2020 config.region = region;
2021
2022 let credentials = alien_aws_clients::AwsCredentialProvider::from_config(config)
2023 .await
2024 .context(ErrorData::BindingSetupFailed {
2025 binding_type: "AWS sandbox".to_string(),
2026 reason: "Failed to create credential provider".to_string(),
2027 })?;
2028
2029 let client = LambdaMicrovmsClient::new(reqwest::Client::new(), credentials);
2030
2031 let egress_connector_arns = aws_binding
2032 .egress_connector_arns
2033 .into_iter()
2034 .map(|value| {
2035 value
2036 .into_value(binding_name, "egressConnectorArns")
2037 .map_err(|_| invalid("egressConnectorArns"))
2038 })
2039 .collect::<Result<Vec<_>>>()?;
2040 if egress_connector_arns.is_empty() != aws_binding.allow_egress {
2044 let reason = if aws_binding.allow_egress {
2045 "sandbox binding declares open egress and also names egress connectors; \
2046 a sandbox cannot be both open and routed through a denying connector"
2047 } else {
2048 "sandbox binding field 'egressConnectorArns' is empty; a MicroVM started \
2049 with no egress connector reaches the public internet"
2050 };
2051 return Err(AlienError::new(ErrorData::BindingConfigInvalid {
2052 binding_name: binding_name.to_string(),
2053 env_var: alien_core::bindings::binding_env_var_name(binding_name),
2054 reason: reason.to_string(),
2055 }));
2056 }
2057
2058 let sandbox: Arc<dyn crate::traits::Sandbox> = Arc::new(AwsSandbox::new(
2059 Arc::new(client),
2060 image_arn,
2061 image_version,
2062 egress_connector_arns,
2063 aws_binding.preview_ports,
2064 aws_binding.idle_pause_seconds,
2065 aws_binding.max_lifetime_seconds,
2066 ));
2067 Ok(sandbox)
2068 }
2069 #[cfg(not(feature = "aws"))]
2072 SandboxBinding::Aws(_) => Err(not_built("aws")),
2073 #[cfg(not(feature = "azure"))]
2074 SandboxBinding::Azure(_) => Err(not_built("azure")),
2075 #[cfg(not(feature = "gcp"))]
2076 SandboxBinding::GcpAgentPlatform(_) => Err(not_built("gcp")),
2077 #[cfg(not(feature = "kubernetes"))]
2078 SandboxBinding::Kubernetes(_) => Err(not_built("kubernetes")),
2079 #[cfg(not(feature = "local"))]
2080 SandboxBinding::Local(_) => Err(not_built("local")),
2081 }
2082 }
2083}
2084
2085#[allow(dead_code)]
2089fn not_built(backend: &str) -> AlienError<ErrorData> {
2093 AlienError::new(ErrorData::OperationNotSupported {
2094 operation: format!("load_sandbox({backend})"),
2095 reason: "this build does not include the sandbox backend for that platform".to_string(),
2096 })
2097}
2098
2099#[cfg(test)]
2100mod tests {
2101 use super::*;
2102 use alien_core::ENV_ALIEN_DEPLOYMENT_TYPE;
2103
2104 #[test]
2108 fn the_substituted_azure_defaults_satisfy_the_plan_time_sizing_rule() {
2109 let declared_as_default = alien_core::Sandbox::new("agent-sbx".to_string())
2110 .code(alien_core::SandboxCode::Image {
2111 image: "ubuntu".to_string(),
2112 })
2113 .limits(alien_core::SandboxLimits {
2114 cpu: DEFAULT_AZURE_CPU.to_string(),
2115 memory: DEFAULT_AZURE_MEMORY.to_string(),
2116 disk: "20Gi".to_string(),
2117 max_processes: None,
2118 })
2119 .egress(alien_core::SandboxEgress::Allow)
2120 .lifecycle(alien_core::SandboxLifecyclePolicy {
2121 max_lifetime_seconds: None,
2122 idle_pause_seconds: None,
2123 })
2124 .build();
2125
2126 declared_as_default.azure_sandbox_limits().expect(
2127 "a sandbox declaring nothing is created with these values, so the rule that would \
2128 have refused them at plan time must accept them",
2129 );
2130 }
2131
2132 fn kubernetes_aws_env() -> HashMap<String, String> {
2133 HashMap::from([
2134 (
2135 ENV_ALIEN_DEPLOYMENT_TYPE.to_string(),
2136 Platform::Kubernetes.as_str().to_string(),
2137 ),
2138 (
2139 ENV_OPERATOR_BASE_PLATFORM.to_string(),
2140 Platform::Aws.as_str().to_string(),
2141 ),
2142 (
2143 "KUBERNETES_SERVICE_HOST".to_string(),
2144 "10.0.0.1".to_string(),
2145 ),
2146 ("KUBERNETES_SERVICE_PORT".to_string(), "443".to_string()),
2147 ("AWS_REGION".to_string(), "us-east-1".to_string()),
2148 ("AWS_ACCOUNT_ID".to_string(), "123456789012".to_string()),
2149 ("AWS_ACCESS_KEY_ID".to_string(), "test".to_string()),
2150 ("AWS_SECRET_ACCESS_KEY".to_string(), "test".to_string()),
2151 ])
2152 }
2153
2154 #[cfg(feature = "kubernetes")]
2155 fn kubernetes_azure_env() -> HashMap<String, String> {
2156 HashMap::from([
2157 (
2158 ENV_ALIEN_DEPLOYMENT_TYPE.to_string(),
2159 Platform::Kubernetes.as_str().to_string(),
2160 ),
2161 (
2162 ENV_OPERATOR_BASE_PLATFORM.to_string(),
2163 Platform::Azure.as_str().to_string(),
2164 ),
2165 (
2166 "KUBERNETES_SERVICE_HOST".to_string(),
2167 "10.0.0.1".to_string(),
2168 ),
2169 ("KUBERNETES_SERVICE_PORT".to_string(), "443".to_string()),
2170 (
2171 "AZURE_SUBSCRIPTION_ID".to_string(),
2172 "00000000-0000-0000-0000-000000000000".to_string(),
2173 ),
2174 (
2175 "AZURE_TENANT_ID".to_string(),
2176 "11111111-1111-1111-1111-111111111111".to_string(),
2177 ),
2178 ("AZURE_REGION".to_string(), "eastus".to_string()),
2179 (
2180 "AZURE_CLIENT_ID".to_string(),
2181 "22222222-2222-2222-2222-222222222222".to_string(),
2182 ),
2183 (
2184 "AZURE_FEDERATED_TOKEN_FILE".to_string(),
2185 "/var/run/secrets/azure/tokens/azure-identity-token".to_string(),
2186 ),
2187 (
2188 "AZURE_AUTHORITY_HOST".to_string(),
2189 "https://login.microsoftonline.com/".to_string(),
2190 ),
2191 ])
2192 }
2193
2194 #[tokio::test]
2195 async fn lazy_env_provider_defers_cloud_client_config_until_binding_use() {
2196 let env = HashMap::from([
2197 (
2198 ENV_ALIEN_DEPLOYMENT_TYPE.to_string(),
2199 Platform::Aws.as_str().to_string(),
2200 ),
2201 ("AWS_EC2_METADATA_DISABLED".to_string(), "true".to_string()),
2202 (
2203 "AWS_PROFILE".to_string(),
2204 "__alien_missing_test_profile__".to_string(),
2205 ),
2206 (
2207 "ALIEN_SECRETS_BINDING".to_string(),
2208 r#"{"service":"parameter-store","vaultPrefix":"test-secrets"}"#.to_string(),
2209 ),
2210 ]);
2211
2212 let provider = BindingsProvider::from_env_lazy(env)
2213 .expect("lazy provider construction should validate binding JSON without AWS config");
2214
2215 let error = provider
2216 .load_vault("secrets")
2217 .await
2218 .expect_err("binding use should still require AWS client config");
2219
2220 assert_eq!(error.code, "CLIENT_CONFIG_INVALID");
2221 }
2222
2223 #[test]
2227 fn malformed_binding_json_fails_at_construction_for_both_lazy_constructors() {
2228 let env = HashMap::from([
2229 (
2230 ENV_ALIEN_DEPLOYMENT_TYPE.to_string(),
2231 Platform::Aws.as_str().to_string(),
2232 ),
2233 ("ALIEN_FILES_BINDING".to_string(), "not-json".to_string()),
2234 ]);
2235
2236 let error = BindingsProvider::from_env_lazy(env.clone())
2237 .expect_err("from_env_lazy must reject malformed binding JSON at construction");
2238 assert_eq!(error.code, "BINDING_CONFIG_INVALID");
2239
2240 let error = BindingsProvider::from_env_deferred(env)
2241 .expect_err("from_env_deferred must reject malformed binding JSON at construction");
2242 assert_eq!(error.code, "BINDING_CONFIG_INVALID");
2243 }
2244
2245 #[cfg(feature = "kubernetes")]
2248 #[tokio::test]
2249 async fn from_env_builds_kubernetes_cloud_config_when_base_platform_is_set() {
2250 let provider = BindingsProvider::from_env(kubernetes_aws_env())
2251 .await
2252 .unwrap();
2253
2254 assert!(provider.client_config.kubernetes_config().is_some());
2255 assert!(provider.client_config.aws_config().is_some());
2256 assert!(matches!(
2257 provider.client_config,
2258 ClientConfig::KubernetesCloud { .. }
2259 ));
2260 }
2261
2262 #[cfg(feature = "kubernetes")]
2263 #[tokio::test]
2264 async fn from_env_builds_kubernetes_cloud_config_for_azure_workload_identity() {
2265 let provider = BindingsProvider::from_env(kubernetes_azure_env())
2266 .await
2267 .unwrap();
2268
2269 assert!(provider.client_config.kubernetes_config().is_some());
2270 assert!(provider.client_config.azure_config().is_some());
2271 assert!(matches!(
2272 provider.client_config,
2273 ClientConfig::KubernetesCloud { .. }
2274 ));
2275 }
2276
2277 #[tokio::test]
2278 async fn from_env_rejects_non_cloud_kubernetes_base_platform() {
2279 let mut env = kubernetes_aws_env();
2280 env.insert(
2281 ENV_OPERATOR_BASE_PLATFORM.to_string(),
2282 Platform::Kubernetes.as_str().to_string(),
2283 );
2284
2285 let error = BindingsProvider::from_env(env).await.unwrap_err();
2286
2287 assert!(error.to_string().contains(ENV_OPERATOR_BASE_PLATFORM));
2288 }
2289
2290 #[tokio::test]
2291 async fn load_storage_for_unconfigured_binding_returns_binding_not_configured() {
2292 let env = HashMap::from([(
2293 ENV_ALIEN_DEPLOYMENT_TYPE.to_string(),
2294 Platform::Local.as_str().to_string(),
2295 )]);
2296 let provider = BindingsProvider::from_env(env)
2297 .await
2298 .expect("provider with no bindings configured should still construct");
2299
2300 let error = provider
2301 .load_storage("files")
2302 .await
2303 .expect_err("binding that was never configured should error");
2304
2305 assert_eq!(error.code, "BINDING_NOT_CONFIGURED");
2306 assert!(
2307 error.to_string().contains("ALIEN_FILES_BINDING"),
2308 "message should name the derived env var, got: {error}"
2309 );
2310 }
2311
2312 #[cfg(feature = "azure")]
2317 #[tokio::test]
2318 async fn an_azure_sandbox_binding_carries_its_disk_image_to_the_provider() {
2319 let env = HashMap::from([
2320 (
2321 ENV_ALIEN_DEPLOYMENT_TYPE.to_string(),
2322 Platform::Azure.as_str().to_string(),
2323 ),
2324 ("AZURE_SUBSCRIPTION_ID".to_string(), "sub".to_string()),
2325 ("AZURE_TENANT_ID".to_string(), "ten".to_string()),
2326 ("AZURE_CLIENT_ID".to_string(), "cli".to_string()),
2327 ("AZURE_CLIENT_SECRET".to_string(), "sec".to_string()),
2328 (
2329 "ALIEN_BOX_BINDING".to_string(),
2330 r#"{"service":"sandbox-azure",
2331 "sandboxGroup":"grp",
2332 "dataPlaneEndpoint":"https://management.swedencentral.azuredevcompute.io",
2333 "region":"swedencentral",
2334 "resourceGroup":"rg",
2335 "diskImage":"my-toolchain",
2336 "egress":{"mode":"deny"}}"#
2337 .to_string(),
2338 ),
2339 ]);
2340 let provider = BindingsProvider::from_env(env)
2341 .await
2342 .expect("provider construction validates only that the binding JSON parses");
2343
2344 let sandbox = provider
2345 .load_sandbox("box")
2346 .await
2347 .expect("an Azure sandbox binding loads");
2348
2349 let azure = sandbox
2350 .as_any()
2351 .downcast_ref::<crate::providers::sandbox::azure::AzureSandbox>()
2352 .expect("an Azure binding builds an Azure provider");
2353 assert_eq!(
2354 azure.disk_image(),
2355 "my-toolchain",
2356 "the declared image must reach the provider, not a literal chosen at construction"
2357 );
2358 }
2359
2360 #[cfg(feature = "aws")]
2364 #[tokio::test]
2365 async fn a_sandbox_binding_whose_egress_fields_disagree_is_refused() {
2366 const CONNECTOR: &str = "arn:aws:lambda:us-east-1:123456789012:network-connector:nc-1";
2367
2368 async fn load(connectors: &str, allow_egress: bool) -> Result<()> {
2369 let env = HashMap::from([
2370 (
2371 ENV_ALIEN_DEPLOYMENT_TYPE.to_string(),
2372 Platform::Aws.as_str().to_string(),
2373 ),
2374 ("AWS_REGION".to_string(), "us-east-1".to_string()),
2375 ("AWS_ACCOUNT_ID".to_string(), "123456789012".to_string()),
2376 ("AWS_ACCESS_KEY_ID".to_string(), "test".to_string()),
2377 ("AWS_SECRET_ACCESS_KEY".to_string(), "test".to_string()),
2378 (
2379 "ALIEN_BOX_BINDING".to_string(),
2380 format!(
2381 r#"{{"service":"sandbox-aws",
2382 "imageArn":"arn:aws:lambda:us-east-1:123456789012:microvm-image:box",
2383 "imageVersion":"1.0",
2384 "region":"us-east-1",
2385 "allowEgress":{allow_egress},
2386 "egressConnectorArns":[{connectors}]}}"#
2387 ),
2388 ),
2389 ]);
2390 BindingsProvider::from_env(env)
2391 .await
2392 .expect("the binding JSON parses")
2393 .load_sandbox("box")
2394 .await
2395 .map(|_| ())
2396 }
2397
2398 let fail_open = load("", false)
2399 .await
2400 .expect_err("an empty connector list with allowEgress false is a fail-open default");
2401 assert_eq!(fail_open.code, "BINDING_CONFIG_INVALID");
2402 assert!(
2403 fail_open.to_string().contains("egressConnectorArns"),
2404 "the message should name the field that was refused, got: {fail_open}"
2405 );
2406
2407 let contradiction = load(&format!(r#""{CONNECTOR}""#), true)
2408 .await
2409 .expect_err("open egress and a denying connector cannot both be declared");
2410 assert_eq!(contradiction.code, "BINDING_CONFIG_INVALID");
2411
2412 load(&format!(r#""{CONNECTOR}""#), false)
2415 .await
2416 .expect("a deny binding names a connector and must load");
2417 load("", true)
2418 .await
2419 .expect("an allow binding names none and must load");
2420 }
2421
2422 #[cfg(feature = "aws")]
2426 #[tokio::test]
2427 async fn a_sandbox_binding_naming_an_execution_role_is_refused() {
2428 let env = HashMap::from([
2429 (
2430 ENV_ALIEN_DEPLOYMENT_TYPE.to_string(),
2431 Platform::Aws.as_str().to_string(),
2432 ),
2433 ("AWS_REGION".to_string(), "us-east-1".to_string()),
2434 ("AWS_ACCOUNT_ID".to_string(), "123456789012".to_string()),
2435 ("AWS_ACCESS_KEY_ID".to_string(), "test".to_string()),
2436 ("AWS_SECRET_ACCESS_KEY".to_string(), "test".to_string()),
2437 (
2438 "ALIEN_BOX_BINDING".to_string(),
2439 r#"{"service":"sandbox-aws",
2440 "imageArn":"arn:aws:lambda:us-east-1:123456789012:microvm-image:box",
2441 "imageVersion":"1.0",
2442 "region":"us-east-1",
2443 "executionRoleArn":"arn:aws:iam::123456789012:role/box",
2444 "egressConnectorArns":["arn:aws:lambda:us-east-1:123456789012:network-connector:nc-1"]}"#
2445 .to_string(),
2446 ),
2447 ]);
2448 let provider = BindingsProvider::from_env(env)
2449 .await
2450 .expect("provider construction only validates that the binding JSON parses");
2451
2452 let error = provider
2453 .load_sandbox("box")
2454 .await
2455 .expect_err("a sandbox binding naming an execution role should be refused");
2456
2457 assert_eq!(error.code, "BINDING_CONFIG_INVALID");
2458 assert!(
2459 error.to_string().contains("executionRoleArn"),
2460 "the message should name the field that was refused, got: {error}"
2461 );
2462 }
2463
2464 #[tokio::test]
2465 async fn load_kv_for_malformed_binding_json_returns_binding_config_invalid_with_env_var() {
2466 let env = HashMap::from([
2467 (
2468 ENV_ALIEN_DEPLOYMENT_TYPE.to_string(),
2469 Platform::Local.as_str().to_string(),
2470 ),
2471 (
2472 "ALIEN_CACHE_BINDING".to_string(),
2473 r#"{"service":"local-kv"}"#.to_string(), ),
2475 ]);
2476 let provider = BindingsProvider::from_env(env)
2477 .await
2478 .expect("provider construction only validates JSON parses, not field completeness");
2479
2480 let error = provider
2481 .load_kv("cache")
2482 .await
2483 .expect_err("binding missing a required field should error");
2484
2485 assert_eq!(error.code, "BINDING_CONFIG_INVALID");
2486 assert!(
2487 error.to_string().contains("ALIEN_CACHE_BINDING"),
2488 "message should name the env var, got: {error}"
2489 );
2490 }
2491
2492 mod selection {
2495 use super::*;
2496 use crate::traits::BindingsProviderApi;
2497 use alien_core::{
2498 ENV_ALIEN_DEPLOYMENT_ID, ENV_ALIEN_DEPLOYMENT_SERVICE_ACCOUNT,
2499 ENV_ALIEN_DEPLOYMENT_TOKEN, ENV_ALIEN_MANAGER_URL, ENV_ALIEN_RESOURCE_ID,
2500 };
2501 use axum::{extract::State, routing::post, Json, Router};
2502 use std::net::SocketAddr;
2503 use std::sync::atomic::{AtomicUsize, Ordering};
2504 use tempfile::TempDir;
2505
2506 async fn mint_handler(State(calls): State<Arc<AtomicUsize>>) -> Json<serde_json::Value> {
2508 calls.fetch_add(1, Ordering::SeqCst);
2509 let expires_at = (chrono::Utc::now() + chrono::Duration::seconds(3600)).to_rfc3339();
2510 Json(serde_json::json!({
2511 "clientConfig": { "platform": "local", "state_directory": "/tmp/alien-sel-test" },
2512 "expiresAt": expires_at,
2513 "principal": "local:mint-test",
2514 }))
2515 }
2516
2517 async fn spawn_mint_server() -> (String, Arc<AtomicUsize>) {
2518 let calls = Arc::new(AtomicUsize::new(0));
2519 let app = Router::new()
2520 .route("/v1/credentials/mint", post(mint_handler))
2521 .with_state(calls.clone());
2522 let listener = tokio::net::TcpListener::bind(SocketAddr::from(([127, 0, 0, 1], 0)))
2523 .await
2524 .expect("bind");
2525 let addr = listener.local_addr().expect("addr");
2526 tokio::spawn(async move {
2527 axum::serve(listener, app).await.expect("serve");
2528 });
2529 (format!("http://{addr}"), calls)
2530 }
2531
2532 fn local_storage_binding(dir: &TempDir) -> String {
2533 format!(
2534 r#"{{"service":"local-storage","storagePath":"{}"}}"#,
2535 dir.path().display()
2536 )
2537 }
2538
2539 fn mint_env(manager_url: &str) -> HashMap<String, String> {
2541 HashMap::from([
2542 (ENV_ALIEN_MANAGER_URL.to_string(), manager_url.to_string()),
2543 (
2544 ENV_ALIEN_DEPLOYMENT_TOKEN.to_string(),
2545 "ax_deploy_tok".to_string(),
2546 ),
2547 (ENV_ALIEN_DEPLOYMENT_ID.to_string(), "dep_1".to_string()),
2548 (
2549 ENV_ALIEN_DEPLOYMENT_SERVICE_ACCOUNT.to_string(),
2550 "management".to_string(),
2551 ),
2552 (ENV_ALIEN_RESOURCE_ID.to_string(), "api".to_string()),
2553 ])
2554 }
2555
2556 #[tokio::test]
2557 async fn native_config_wins_and_never_mints() {
2558 let (base_url, calls) = spawn_mint_server().await;
2562 let dir = TempDir::new().expect("tempdir");
2563
2564 let mut env = mint_env(&base_url);
2565 env.insert(
2566 ENV_ALIEN_DEPLOYMENT_TYPE.to_string(),
2567 Platform::Local.as_str().to_string(),
2568 );
2569 env.insert(
2570 "ALIEN_FILES_BINDING".to_string(),
2571 local_storage_binding(&dir),
2572 );
2573
2574 let provider = BindingsProvider::from_env_lazy(env).expect("lazy construct");
2575 provider
2576 .load_storage("files")
2577 .await
2578 .expect("native local storage should load");
2579
2580 assert_eq!(
2581 calls.load(Ordering::SeqCst),
2582 0,
2583 "native credentials must never trigger a mint"
2584 );
2585 }
2586
2587 #[tokio::test]
2588 async fn mints_when_native_config_unavailable() {
2589 let (base_url, calls) = spawn_mint_server().await;
2593 let dir = TempDir::new().expect("tempdir");
2594
2595 let mut env = mint_env(&base_url);
2596 env.insert(
2597 ENV_ALIEN_DEPLOYMENT_TYPE.to_string(),
2598 Platform::Aws.as_str().to_string(),
2599 );
2600 env.insert("AWS_EC2_METADATA_DISABLED".to_string(), "true".to_string());
2601 env.insert(
2602 "AWS_PROFILE".to_string(),
2603 "__alien_missing_test_profile__".to_string(),
2604 );
2605 env.insert(
2606 "ALIEN_FILES_BINDING".to_string(),
2607 local_storage_binding(&dir),
2608 );
2609
2610 let provider = BindingsProvider::from_env_lazy(env).expect("lazy construct");
2611 provider
2612 .load_storage("files")
2613 .await
2614 .expect("mint path should resolve a usable config");
2615
2616 assert_eq!(
2617 calls.load(Ordering::SeqCst),
2618 1,
2619 "unavailable native credentials must trigger exactly one mint"
2620 );
2621 }
2622
2623 #[tokio::test]
2624 async fn no_mint_contract_preserves_original_from_env_error() {
2625 let dir = TempDir::new().expect("tempdir");
2628 let env = HashMap::from([
2629 (
2630 ENV_ALIEN_DEPLOYMENT_TYPE.to_string(),
2631 Platform::Aws.as_str().to_string(),
2632 ),
2633 ("AWS_EC2_METADATA_DISABLED".to_string(), "true".to_string()),
2634 (
2635 "AWS_PROFILE".to_string(),
2636 "__alien_missing_test_profile__".to_string(),
2637 ),
2638 (
2639 "ALIEN_FILES_BINDING".to_string(),
2640 local_storage_binding(&dir),
2641 ),
2642 ]);
2643
2644 let provider = BindingsProvider::from_env_lazy(env).expect("lazy construct");
2645 let error = provider
2646 .load_storage("files")
2647 .await
2648 .expect_err("no creds and no mint contract must error");
2649
2650 assert_eq!(error.code, "CLIENT_CONFIG_INVALID");
2651 }
2652 }
2653}