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