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