1use crate::{
4 error::{ErrorData, Result},
5 traits::{ArtifactRegistry, BindingsProviderApi, Build, Kv, Queue, Storage, Vault, Worker},
6};
7
8use alien_client_config::ClientConfigExt;
9use alien_core::{ClientConfig, Platform, StackState, ENV_ALIEN_BASE_PLATFORM};
10use alien_error::{AlienError, Context, IntoAlienError};
11use async_trait::async_trait;
12use std::{any::Any, collections::HashMap, sync::Arc};
13use tokio::sync::RwLock;
14
15#[derive(Debug, Clone)]
23pub struct BindingsProvider {
24 client_config: ClientConfig,
25 bindings: HashMap<String, serde_json::Value>,
26 cache: Arc<RwLock<HashMap<String, Box<dyn Any + Send + Sync>>>>,
30}
31
32impl BindingsProvider {
33 pub fn new(
37 client_config: ClientConfig,
38 bindings: HashMap<String, serde_json::Value>,
39 ) -> Result<Self> {
40 Ok(Self {
41 client_config,
42 bindings,
43 cache: Arc::new(RwLock::new(HashMap::new())),
44 })
45 }
46
47 async fn get_cached<T: Clone + Send + Sync + 'static>(
49 &self,
50 trait_name: &str,
51 binding_name: &str,
52 ) -> Option<T> {
53 let cache_key = format!("{}:{}", trait_name, binding_name);
54 let cache = self.cache.read().await;
55 cache
56 .get(&cache_key)
57 .and_then(|boxed| boxed.downcast_ref::<T>())
58 .cloned()
59 }
60
61 async fn put_cache<T: Clone + Send + Sync + 'static>(
63 &self,
64 trait_name: &str,
65 binding_name: &str,
66 value: T,
67 ) {
68 let cache_key = format!("{}:{}", trait_name, binding_name);
69 let mut cache = self.cache.write().await;
70 cache.insert(cache_key, Box::new(value));
71 }
72
73 pub async fn from_env(env: HashMap<String, String>) -> Result<Self> {
78 let platform = crate::get_platform_from_env(&env)?;
80
81 let client_config = Self::client_config_from_env(platform, &env).await?;
83
84 let bindings = Self::parse_bindings_from_env(&env)?;
86
87 Self::new(client_config, bindings)
88 }
89
90 async fn client_config_from_env(
91 platform: Platform,
92 env: &HashMap<String, String>,
93 ) -> Result<ClientConfig> {
94 if platform != Platform::Kubernetes {
95 return Self::load_client_config_from_env(platform, env).await;
96 }
97
98 let Some(base_platform) = Self::base_platform_from_env(env)? else {
99 return Self::load_client_config_from_env(platform, env).await;
100 };
101
102 let kubernetes = match Self::load_client_config_from_env(Platform::Kubernetes, env).await? {
103 ClientConfig::Kubernetes(kubernetes) => kubernetes,
104 _ => unreachable!("kubernetes platform must produce a Kubernetes client config"),
105 };
106 let cloud = Self::load_client_config_from_env(base_platform, env).await?;
107
108 Ok(ClientConfig::KubernetesCloud {
109 kubernetes,
110 cloud: Box::new(cloud),
111 })
112 }
113
114 fn base_platform_from_env(env: &HashMap<String, String>) -> Result<Option<Platform>> {
115 let Some(base_platform) = env.get(ENV_ALIEN_BASE_PLATFORM) else {
116 return Ok(None);
117 };
118
119 let parsed: Platform = base_platform.parse().map_err(|reason| {
120 AlienError::new(ErrorData::InvalidEnvironmentVariable {
121 variable_name: ENV_ALIEN_BASE_PLATFORM.to_string(),
122 value: base_platform.clone(),
123 reason,
124 })
125 })?;
126
127 if !matches!(parsed, Platform::Aws | Platform::Gcp | Platform::Azure) {
128 return Err(AlienError::new(ErrorData::InvalidEnvironmentVariable {
129 variable_name: ENV_ALIEN_BASE_PLATFORM.to_string(),
130 value: base_platform.clone(),
131 reason: "Kubernetes base platform must be aws, gcp, or azure".to_string(),
132 }));
133 }
134
135 Ok(Some(parsed))
136 }
137
138 async fn load_client_config_from_env(
139 platform: Platform,
140 env: &HashMap<String, String>,
141 ) -> Result<ClientConfig> {
142 ClientConfig::from_env(platform, env).await.map_err(|e| {
143 AlienError::new(ErrorData::ClientConfigInvalid {
144 platform,
145 message: format!("Failed to load client config: {}", e),
146 })
147 })
148 }
149
150 fn parse_bindings_from_env(
152 env: &HashMap<String, String>,
153 ) -> Result<HashMap<String, serde_json::Value>> {
154 let mut bindings = HashMap::new();
155 for (key, value) in env {
156 if key.starts_with("ALIEN_") && key.ends_with("_BINDING") {
157 let binding_name = key
158 .strip_prefix("ALIEN_")
159 .unwrap()
160 .strip_suffix("_BINDING")
161 .unwrap()
162 .to_lowercase()
163 .replace('_', "-");
164 let parsed: serde_json::Value = serde_json::from_str(value)
165 .into_alien_error()
166 .context(ErrorData::BindingConfigInvalid {
167 binding_name: binding_name.clone(),
168 reason: "Failed to parse binding JSON".to_string(),
169 })?;
170 bindings.insert(binding_name, parsed);
171 }
172 }
173 Ok(bindings)
174 }
175
176 pub fn from_stack_state(stack_state: &StackState, client_config: ClientConfig) -> Result<Self> {
187 let bindings = stack_state
188 .resources
189 .iter()
190 .filter_map(|(id, state)| {
191 state
192 .remote_binding_params
193 .as_ref()
194 .map(|p| (id.clone(), p.clone()))
195 })
196 .collect();
197
198 Self::new(client_config, bindings)
199 }
200
201 #[cfg(feature = "platform-sdk")]
218 pub async fn for_remote_deployment(
219 deployment_id: &str,
220 token: &str,
221 api_base_url: Option<&str>,
222 ) -> Result<Self> {
223 let base_url = api_base_url.unwrap_or("https://api.alien.dev");
224
225 let auth_value = format!("Bearer {}", token);
228 let mut headers = reqwest::header::HeaderMap::new();
229 headers.insert(
230 reqwest::header::AUTHORIZATION,
231 reqwest::header::HeaderValue::from_str(&auth_value)
232 .into_alien_error()
233 .context(ErrorData::RemoteAccessFailed {
234 operation: "build Platform API client with token".to_string(),
235 })?,
236 );
237
238 let authed_http_client = reqwest::Client::builder()
239 .default_headers(headers)
240 .build()
241 .into_alien_error()
242 .context(ErrorData::RemoteAccessFailed {
243 operation: "build Platform API HTTP client".to_string(),
244 })?;
245
246 let sdk_client = alien_platform_api::Client::new_with_client(base_url, authed_http_client);
247
248 let deployment_response = sdk_client
250 .get_deployment()
251 .id(deployment_id)
252 .send()
253 .await
254 .into_alien_error()
255 .context(ErrorData::RemoteAccessFailed {
256 operation: "fetch deployment from Platform API".to_string(),
257 })?
258 .into_inner();
259
260 let manager_id = deployment_response.manager_id.ok_or_else(|| {
262 AlienError::new(ErrorData::RemoteAccessFailed {
263 operation: "fetch manager from Platform API".to_string(),
264 })
265 })?;
266
267 let manager_response = sdk_client
268 .get_manager()
269 .id(&manager_id.to_string())
270 .send()
271 .await
272 .into_alien_error()
273 .context(ErrorData::RemoteAccessFailed {
274 operation: "fetch manager from Platform API".to_string(),
275 })?
276 .into_inner();
277
278 let stack_state = deployment_response.stack_state.as_ref().ok_or_else(|| {
282 AlienError::new(ErrorData::RemoteAccessFailed {
283 operation: "Deployment has no stack state (not deployed yet)".to_string(),
284 })
285 })?;
286
287 let alien_stack_state = conversions::convert_stack_state(stack_state)?;
288
289 let manager_url = manager_response.url.ok_or_else(|| {
293 AlienError::new(ErrorData::RemoteAccessFailed {
294 operation: "fetch manager URL from Platform API".to_string(),
295 })
296 })?;
297
298 let http_client = reqwest::Client::new();
299 let client_config = http_client
300 .post(format!("{}/v1/deployment/resolve-credentials", manager_url))
301 .bearer_auth(token)
302 .json(&serde_json::json!({
303 "deploymentId": deployment_id,
304 }))
305 .send()
306 .await
307 .into_alien_error()
308 .context(ErrorData::RemoteAccessFailed {
309 operation: "resolve credentials from manager".to_string(),
310 })?
311 .json::<ResolveCredentialsResponse>()
312 .await
313 .into_alien_error()
314 .context(ErrorData::RemoteAccessFailed {
315 operation: "parse credentials response".to_string(),
316 })?
317 .client_config;
318
319 Self::from_stack_state(&alien_stack_state, client_config)
321 }
322}
323
324#[cfg(feature = "platform-sdk")]
325#[derive(serde::Deserialize)]
326#[serde(rename_all = "camelCase")]
327struct ResolveCredentialsResponse {
328 client_config: ClientConfig,
329}
330
331#[async_trait]
332impl BindingsProviderApi for BindingsProvider {
333 async fn load_storage(&self, binding_name: &str) -> Result<Arc<dyn Storage>> {
334 if let Some(cached) = self
335 .get_cached::<Arc<dyn Storage>>("storage", binding_name)
336 .await
337 {
338 return Ok(cached);
339 }
340
341 use alien_core::bindings::StorageBinding;
342
343 let binding_json = self.bindings.get(binding_name).ok_or_else(|| {
345 AlienError::new(ErrorData::BindingConfigInvalid {
346 binding_name: binding_name.to_string(),
347 reason: "Binding not found".to_string(),
348 })
349 })?;
350
351 let binding: StorageBinding = serde_json::from_value(binding_json.clone())
353 .into_alien_error()
354 .context(ErrorData::BindingConfigInvalid {
355 binding_name: binding_name.to_string(),
356 reason: "Failed to parse storage binding".to_string(),
357 })?;
358
359 let result: Arc<dyn Storage> = match binding {
360 #[cfg(feature = "aws")]
361 StorageBinding::S3(config) => {
362 use crate::providers::storage::aws_s3::S3Storage;
363
364 let aws_config = self.client_config.aws_config().ok_or_else(|| {
366 AlienError::new(ErrorData::ClientConfigInvalid {
367 platform: Platform::Aws,
368 message: "AWS config not available".to_string(),
369 })
370 })?;
371
372 let credentials =
373 alien_aws_clients::AwsCredentialProvider::from_config(aws_config.clone())
374 .await
375 .context(ErrorData::BindingSetupFailed {
376 binding_type: "AWS S3 storage".to_string(),
377 reason: "Failed to create credential provider".to_string(),
378 })?;
379
380 let bucket_name = config
382 .bucket_name
383 .into_value(binding_name, "bucket_name")
384 .context(ErrorData::BindingConfigInvalid {
385 binding_name: binding_name.to_string(),
386 reason: "Failed to extract bucket_name from S3 binding".to_string(),
387 })?;
388
389 let storage: Arc<dyn Storage> = Arc::new(S3Storage::new(bucket_name, credentials)?);
390 Ok(storage)
391 }
392 #[cfg(not(feature = "aws"))]
393 StorageBinding::S3 { .. } => Err(AlienError::new(ErrorData::FeatureNotEnabled {
394 feature: "aws".to_string(),
395 })),
396
397 #[cfg(feature = "azure")]
398 StorageBinding::Blob(config) => {
399 use crate::providers::storage::azure_blob::BlobStorage;
400
401 let azure_config = self.client_config.azure_config().ok_or_else(|| {
402 AlienError::new(ErrorData::ClientConfigInvalid {
403 platform: Platform::Azure,
404 message: "Azure config not available".to_string(),
405 })
406 })?;
407
408 let container_name = config
410 .container_name
411 .into_value(binding_name, "container_name")
412 .context(ErrorData::BindingConfigInvalid {
413 binding_name: binding_name.to_string(),
414 reason: "Failed to extract container_name from Blob binding".to_string(),
415 })?;
416
417 let account_name = config
418 .account_name
419 .into_value(binding_name, "account_name")
420 .context(ErrorData::BindingConfigInvalid {
421 binding_name: binding_name.to_string(),
422 reason: "Failed to extract account_name from Blob binding".to_string(),
423 })?;
424
425 let storage: Arc<dyn Storage> = Arc::new(BlobStorage::new(
426 container_name,
427 account_name,
428 azure_config,
429 )?);
430 Ok(storage)
431 }
432 #[cfg(not(feature = "azure"))]
433 StorageBinding::Blob { .. } => Err(AlienError::new(ErrorData::FeatureNotEnabled {
434 feature: "azure".to_string(),
435 })),
436
437 #[cfg(feature = "gcp")]
438 StorageBinding::Gcs(config) => {
439 use crate::providers::storage::gcp_gcs::GcsStorage;
440
441 let gcp_config = self.client_config.gcp_config().ok_or_else(|| {
442 AlienError::new(ErrorData::ClientConfigInvalid {
443 platform: Platform::Gcp,
444 message: "GCP config not available".to_string(),
445 })
446 })?;
447
448 let bucket_name = config
450 .bucket_name
451 .into_value(binding_name, "bucket_name")
452 .context(ErrorData::BindingConfigInvalid {
453 binding_name: binding_name.to_string(),
454 reason: "Failed to extract bucket_name from Gcs binding".to_string(),
455 })?;
456
457 let storage: Arc<dyn Storage> = Arc::new(GcsStorage::new(bucket_name, gcp_config)?);
458 Ok(storage)
459 }
460 #[cfg(not(feature = "gcp"))]
461 StorageBinding::Gcs { .. } => Err(AlienError::new(ErrorData::FeatureNotEnabled {
462 feature: "gcp".to_string(),
463 })),
464
465 #[cfg(feature = "local")]
466 StorageBinding::Local(config) => {
467 use crate::providers::storage::local::LocalStorage;
468
469 let storage_path = config
471 .storage_path
472 .into_value(binding_name, "storage_path")
473 .context(ErrorData::BindingConfigInvalid {
474 binding_name: binding_name.to_string(),
475 reason: "Failed to extract storage_path from Local binding".to_string(),
476 })?;
477
478 let storage: Arc<dyn Storage> = Arc::new(LocalStorage::new(storage_path)?);
479 Ok(storage)
480 }
481 #[cfg(not(feature = "local"))]
482 StorageBinding::Local { .. } => Err(AlienError::new(ErrorData::FeatureNotEnabled {
483 feature: "local".to_string(),
484 })),
485 }?;
486
487 self.put_cache("storage", binding_name, result.clone())
488 .await;
489 Ok(result)
490 }
491
492 async fn load_build(&self, binding_name: &str) -> Result<Arc<dyn Build>> {
493 use alien_core::bindings::BuildBinding;
494
495 let binding_json = self.bindings.get(binding_name).ok_or_else(|| {
496 AlienError::new(ErrorData::BindingConfigInvalid {
497 binding_name: binding_name.to_string(),
498 reason: "Binding not found".to_string(),
499 })
500 })?;
501
502 let binding: BuildBinding = serde_json::from_value(binding_json.clone())
503 .into_alien_error()
504 .context(ErrorData::BindingConfigInvalid {
505 binding_name: binding_name.to_string(),
506 reason: "Failed to parse build binding".to_string(),
507 })?;
508
509 match binding {
510 #[cfg(feature = "aws")]
511 BuildBinding::Codebuild { .. } => {
512 use crate::providers::build::codebuild::CodebuildBuild;
513
514 let aws_config = self.client_config.aws_config().ok_or_else(|| {
515 AlienError::new(ErrorData::ClientConfigInvalid {
516 platform: Platform::Aws,
517 message: "AWS config not available".to_string(),
518 })
519 })?;
520 let credentials =
521 alien_aws_clients::AwsCredentialProvider::from_config(aws_config.clone())
522 .await
523 .context(ErrorData::ClientConfigInvalid {
524 platform: Platform::Aws,
525 message: "Failed to create AWS credential provider".to_string(),
526 })?;
527
528 let build = Arc::new(
529 CodebuildBuild::new(binding_name.to_string(), binding, &credentials)
530 .await
531 .context(ErrorData::BindingConfigInvalid {
532 binding_name: binding_name.to_string(),
533 reason: "Failed to initialize AWS CodeBuild client".to_string(),
534 })?,
535 );
536 Ok(build)
537 }
538 #[cfg(not(feature = "aws"))]
539 BuildBinding::Codebuild { .. } => Err(AlienError::new(ErrorData::FeatureNotEnabled {
540 feature: "aws".to_string(),
541 })),
542
543 #[cfg(feature = "azure")]
544 BuildBinding::Aca { .. } => {
545 use crate::providers::build::aca::AcaBuild;
546
547 let azure_config = self.client_config.azure_config().ok_or_else(|| {
548 AlienError::new(ErrorData::ClientConfigInvalid {
549 platform: Platform::Azure,
550 message: "Azure config not available".to_string(),
551 })
552 })?;
553
554 let build = Arc::new(
555 AcaBuild::new(binding_name.to_string(), binding, azure_config)
556 .await
557 .context(ErrorData::BindingConfigInvalid {
558 binding_name: binding_name.to_string(),
559 reason: "Failed to initialize Azure Container Apps build".to_string(),
560 })?,
561 );
562 Ok(build)
563 }
564 #[cfg(not(feature = "azure"))]
565 BuildBinding::Aca { .. } => Err(AlienError::new(ErrorData::FeatureNotEnabled {
566 feature: "azure".to_string(),
567 })),
568
569 #[cfg(feature = "gcp")]
570 BuildBinding::Cloudbuild { .. } => {
571 use crate::providers::build::cloudbuild::CloudbuildBuild;
572
573 let gcp_config = self.client_config.gcp_config().ok_or_else(|| {
574 AlienError::new(ErrorData::ClientConfigInvalid {
575 platform: Platform::Gcp,
576 message: "GCP config not available".to_string(),
577 })
578 })?;
579
580 let build = Arc::new(
581 CloudbuildBuild::new(binding_name.to_string(), binding, gcp_config)
582 .await
583 .context(ErrorData::BindingConfigInvalid {
584 binding_name: binding_name.to_string(),
585 reason: "Failed to initialize GCP Cloud Build client".to_string(),
586 })?,
587 );
588 Ok(build)
589 }
590 #[cfg(not(feature = "gcp"))]
591 BuildBinding::Cloudbuild { .. } => Err(AlienError::new(ErrorData::FeatureNotEnabled {
592 feature: "gcp".to_string(),
593 })),
594
595 #[cfg(feature = "local")]
596 BuildBinding::Local { .. } => {
597 use crate::providers::build::local::LocalBuild;
598
599 let build = Arc::new(LocalBuild::new(binding_name.to_string(), binding)?);
600 Ok(build)
601 }
602 #[cfg(not(feature = "local"))]
603 BuildBinding::Local { .. } => Err(AlienError::new(ErrorData::FeatureNotEnabled {
604 feature: "local".to_string(),
605 })),
606
607 #[cfg(feature = "kubernetes")]
608 BuildBinding::Kubernetes { .. } => {
609 use crate::providers::build::kubernetes::KubernetesBuild;
610
611 let build =
612 Arc::new(KubernetesBuild::new(binding_name.to_string(), binding).await?);
613 Ok(build)
614 }
615 #[cfg(not(feature = "kubernetes"))]
616 BuildBinding::Kubernetes { .. } => Err(AlienError::new(ErrorData::FeatureNotEnabled {
617 feature: "kubernetes".to_string(),
618 })),
619 }
620 }
621
622 async fn load_artifact_registry(
623 &self,
624 binding_name: &str,
625 ) -> Result<Arc<dyn ArtifactRegistry>> {
626 if let Some(cached) = self
627 .get_cached::<Arc<dyn ArtifactRegistry>>("artifact_registry", binding_name)
628 .await
629 {
630 return Ok(cached);
631 }
632
633 use alien_core::bindings::ArtifactRegistryBinding;
634
635 let binding_json = self.bindings.get(binding_name).ok_or_else(|| {
636 AlienError::new(ErrorData::BindingConfigInvalid {
637 binding_name: binding_name.to_string(),
638 reason: "Binding not found".to_string(),
639 })
640 })?;
641
642 let binding: ArtifactRegistryBinding = serde_json::from_value(binding_json.clone())
643 .into_alien_error()
644 .context(ErrorData::BindingConfigInvalid {
645 binding_name: binding_name.to_string(),
646 reason: "Failed to parse artifact registry binding".to_string(),
647 })?;
648
649 let registry: Arc<dyn ArtifactRegistry> = match binding {
650 #[cfg(feature = "aws")]
651 ArtifactRegistryBinding::Ecr { .. } => {
652 use crate::providers::artifact_registry::ecr::EcrArtifactRegistry;
653
654 let aws_config = self.client_config.aws_config().ok_or_else(|| {
655 AlienError::new(ErrorData::ClientConfigInvalid {
656 platform: Platform::Aws,
657 message: "AWS config not available".to_string(),
658 })
659 })?;
660 let credentials =
661 alien_aws_clients::AwsCredentialProvider::from_config(aws_config.clone())
662 .await
663 .context(ErrorData::ClientConfigInvalid {
664 platform: Platform::Aws,
665 message: "Failed to create AWS credential provider".to_string(),
666 })?;
667
668 let registry: Arc<dyn ArtifactRegistry> = Arc::new(
669 EcrArtifactRegistry::new(binding_name.to_string(), binding, &credentials)
670 .await
671 .context(ErrorData::BindingConfigInvalid {
672 binding_name: binding_name.to_string(),
673 reason: "Failed to initialize AWS ECR artifact registry".to_string(),
674 })?,
675 );
676 Ok(registry)
677 }
678 #[cfg(not(feature = "aws"))]
679 ArtifactRegistryBinding::Ecr { .. } => {
680 Err(AlienError::new(ErrorData::FeatureNotEnabled {
681 feature: "aws".to_string(),
682 }))
683 }
684
685 #[cfg(feature = "azure")]
686 ArtifactRegistryBinding::Acr { .. } => {
687 use crate::providers::artifact_registry::acr::AcrArtifactRegistry;
688
689 let azure_config = self.client_config.azure_config().ok_or_else(|| {
690 AlienError::new(ErrorData::ClientConfigInvalid {
691 platform: Platform::Azure,
692 message: "Azure config not available".to_string(),
693 })
694 })?;
695
696 let registry: Arc<dyn ArtifactRegistry> = Arc::new(
697 AcrArtifactRegistry::new(binding_name.to_string(), binding, azure_config)
698 .await
699 .context(ErrorData::BindingConfigInvalid {
700 binding_name: binding_name.to_string(),
701 reason: "Failed to initialize Azure ACR artifact registry".to_string(),
702 })?,
703 );
704 Ok(registry)
705 }
706 #[cfg(not(feature = "azure"))]
707 ArtifactRegistryBinding::Acr { .. } => {
708 Err(AlienError::new(ErrorData::FeatureNotEnabled {
709 feature: "azure".to_string(),
710 }))
711 }
712
713 #[cfg(feature = "gcp")]
714 ArtifactRegistryBinding::Gar { .. } => {
715 use crate::providers::artifact_registry::gar::GarArtifactRegistry;
716
717 let gcp_config = self.client_config.gcp_config().ok_or_else(|| {
718 AlienError::new(ErrorData::ClientConfigInvalid {
719 platform: Platform::Gcp,
720 message: "GCP config not available".to_string(),
721 })
722 })?;
723
724 let registry: Arc<dyn ArtifactRegistry> = Arc::new(
725 GarArtifactRegistry::new(binding_name.to_string(), binding, gcp_config)
726 .await
727 .context(ErrorData::BindingConfigInvalid {
728 binding_name: binding_name.to_string(),
729 reason: "Failed to initialize GCP GAR artifact registry".to_string(),
730 })?,
731 );
732 Ok(registry)
733 }
734 #[cfg(not(feature = "gcp"))]
735 ArtifactRegistryBinding::Gar { .. } => {
736 Err(AlienError::new(ErrorData::FeatureNotEnabled {
737 feature: "gcp".to_string(),
738 }))
739 }
740
741 #[cfg(feature = "local")]
742 ArtifactRegistryBinding::Local { .. } => {
743 use crate::providers::artifact_registry::local::LocalArtifactRegistry;
744
745 let registry: Arc<dyn ArtifactRegistry> = Arc::new(
746 LocalArtifactRegistry::new(binding_name.to_string(), binding.clone()).await?,
747 );
748 Ok(registry)
749 }
750 #[cfg(not(feature = "local"))]
751 ArtifactRegistryBinding::Local { .. } => {
752 Err(AlienError::new(ErrorData::FeatureNotEnabled {
753 feature: "local".to_string(),
754 }))
755 }
756 }?;
757
758 self.put_cache("artifact_registry", binding_name, registry.clone())
759 .await;
760 Ok(registry)
761 }
762
763 async fn load_vault(&self, binding_name: &str) -> Result<Arc<dyn Vault>> {
764 if let Some(cached) = self
765 .get_cached::<Arc<dyn Vault>>("vault", binding_name)
766 .await
767 {
768 return Ok(cached);
769 }
770
771 use alien_core::bindings::VaultBinding;
772
773 let binding_json = self.bindings.get(binding_name).ok_or_else(|| {
774 AlienError::new(ErrorData::BindingConfigInvalid {
775 binding_name: binding_name.to_string(),
776 reason: "Binding not found".to_string(),
777 })
778 })?;
779
780 let binding: VaultBinding = serde_json::from_value(binding_json.clone())
781 .into_alien_error()
782 .context(ErrorData::BindingConfigInvalid {
783 binding_name: binding_name.to_string(),
784 reason: "Failed to parse vault binding".to_string(),
785 })?;
786
787 let result: Arc<dyn Vault> = match binding {
788 #[cfg(feature = "aws")]
789 VaultBinding::ParameterStore(config) => {
790 use crate::providers::vault::aws_parameter_store::AwsParameterStoreVault;
791 use alien_aws_clients::ssm::SsmClient;
792
793 let aws_config = self.client_config.aws_config().ok_or_else(|| {
794 AlienError::new(ErrorData::ClientConfigInvalid {
795 platform: Platform::Aws,
796 message: "AWS config not available".to_string(),
797 })
798 })?;
799 let credentials =
800 alien_aws_clients::AwsCredentialProvider::from_config(aws_config.clone())
801 .await
802 .context(ErrorData::ClientConfigInvalid {
803 platform: Platform::Aws,
804 message: "Failed to create AWS credential provider".to_string(),
805 })?;
806
807 let client = Arc::new(SsmClient::new(
808 crate::http_client::create_http_client(),
809 credentials,
810 ));
811
812 let vault_prefix = config
814 .vault_prefix
815 .into_value(&binding_name, "vault_prefix")
816 .context(ErrorData::BindingConfigInvalid {
817 binding_name: binding_name.to_string(),
818 reason: "Failed to extract vault_prefix from ParameterStore binding"
819 .to_string(),
820 })?;
821
822 let vault: Arc<dyn Vault> =
823 Arc::new(AwsParameterStoreVault::new(client, vault_prefix));
824 Ok(vault)
825 }
826 #[cfg(not(feature = "aws"))]
827 VaultBinding::ParameterStore(_) => Err(AlienError::new(ErrorData::FeatureNotEnabled {
828 feature: "aws".to_string(),
829 })),
830
831 #[cfg(feature = "azure")]
832 VaultBinding::KeyVault(config) => {
833 use crate::providers::vault::azure_key_vault::AzureKeyVault;
834 use alien_azure_clients::keyvault::AzureKeyVaultSecretsClient;
835 use alien_azure_clients::AzureTokenCache;
836
837 let azure_config = self.client_config.azure_config().ok_or_else(|| {
838 AlienError::new(ErrorData::ClientConfigInvalid {
839 platform: Platform::Azure,
840 message: "Azure config not available".to_string(),
841 })
842 })?;
843
844 let client = Arc::new(AzureKeyVaultSecretsClient::new(
845 crate::http_client::create_http_client(),
846 AzureTokenCache::new(azure_config.clone()),
847 ));
848
849 let vault_name = config
851 .vault_name
852 .into_value(&binding_name, "vault_name")
853 .context(ErrorData::BindingConfigInvalid {
854 binding_name: binding_name.to_string(),
855 reason: "Failed to extract vault_name from KeyVault binding".to_string(),
856 })?;
857
858 let vault_base_url = format!("https://{}.vault.azure.net", vault_name);
861
862 let vault: Arc<dyn Vault> = Arc::new(AzureKeyVault::new(client, vault_base_url));
863 Ok(vault)
864 }
865 #[cfg(not(feature = "azure"))]
866 VaultBinding::KeyVault(_) => Err(AlienError::new(ErrorData::FeatureNotEnabled {
867 feature: "azure".to_string(),
868 })),
869
870 #[cfg(feature = "gcp")]
871 VaultBinding::SecretManager(config) => {
872 use crate::providers::vault::gcp_secret_manager::GcpSecretManagerVault;
873 use alien_gcp_clients::secret_manager::SecretManagerClient;
874
875 let gcp_config = self.client_config.gcp_config().ok_or_else(|| {
876 AlienError::new(ErrorData::ClientConfigInvalid {
877 platform: Platform::Gcp,
878 message: "GCP config not available".to_string(),
879 })
880 })?;
881
882 let client = Arc::new(SecretManagerClient::new(
883 crate::http_client::create_http_client(),
884 gcp_config.clone(),
885 ));
886
887 let vault_prefix = config
889 .vault_prefix
890 .into_value(&binding_name, "vault_prefix")
891 .context(ErrorData::BindingConfigInvalid {
892 binding_name: binding_name.to_string(),
893 reason: "Failed to extract vault_prefix from SecretManager binding"
894 .to_string(),
895 })?;
896
897 let vault: Arc<dyn Vault> = Arc::new(GcpSecretManagerVault::new(
898 client,
899 vault_prefix,
900 gcp_config.project_id.clone(),
901 ));
902 Ok(vault)
903 }
904 #[cfg(not(feature = "gcp"))]
905 VaultBinding::SecretManager(_) => Err(AlienError::new(ErrorData::FeatureNotEnabled {
906 feature: "gcp".to_string(),
907 })),
908
909 #[cfg(feature = "local")]
910 VaultBinding::Local(config) => {
911 use crate::providers::vault::local::LocalVault;
912
913 let vault_dir = config
914 .data_dir
915 .into_value(binding_name, "data_dir")
916 .context(ErrorData::BindingConfigInvalid {
917 binding_name: binding_name.to_string(),
918 reason: "Failed to extract data_dir from vault binding".to_string(),
919 })?;
920
921 let vault: Arc<dyn Vault> = Arc::new(LocalVault::new(
922 binding_name.to_string(),
923 std::path::PathBuf::from(vault_dir),
924 ));
925 Ok(vault)
926 }
927 #[cfg(not(feature = "local"))]
928 VaultBinding::Local { .. } => Err(AlienError::new(ErrorData::FeatureNotEnabled {
929 feature: "local".to_string(),
930 })),
931
932 #[cfg(feature = "kubernetes")]
933 VaultBinding::KubernetesSecret(config) => {
934 use crate::providers::vault::kubernetes_secret::KubernetesSecretVault;
935 use alien_k8s_clients::{secrets::SecretsApi, KubernetesClient};
936
937 let kubernetes_config =
938 self.client_config.kubernetes_config().ok_or_else(|| {
939 AlienError::new(ErrorData::ClientConfigInvalid {
940 platform: Platform::Kubernetes,
941 message: "Kubernetes config not available".to_string(),
942 })
943 })?;
944
945 let kubernetes_client = KubernetesClient::new(kubernetes_config.clone())
946 .await
947 .context(ErrorData::CloudPlatformError {
948 message: "Failed to create Kubernetes client for vault".to_string(),
949 resource_id: None,
950 })?;
951
952 let client: Arc<dyn SecretsApi> = Arc::new(kubernetes_client);
953
954 let namespace = config
956 .namespace
957 .into_value(binding_name, "namespace")
958 .context(ErrorData::BindingConfigInvalid {
959 binding_name: binding_name.to_string(),
960 reason: "Failed to extract namespace from KubernetesSecret binding"
961 .to_string(),
962 })?;
963
964 let vault_prefix = config
965 .vault_prefix
966 .into_value(binding_name, "vault_prefix")
967 .context(ErrorData::BindingConfigInvalid {
968 binding_name: binding_name.to_string(),
969 reason: "Failed to extract vault_prefix from KubernetesSecret binding"
970 .to_string(),
971 })?;
972
973 let vault: Arc<dyn Vault> =
974 Arc::new(KubernetesSecretVault::new(client, namespace, vault_prefix));
975 Ok(vault)
976 }
977 #[cfg(not(feature = "kubernetes"))]
978 VaultBinding::KubernetesSecret(_) => {
979 Err(AlienError::new(ErrorData::FeatureNotEnabled {
980 feature: "kubernetes".to_string(),
981 }))
982 }
983 }?;
984
985 self.put_cache("vault", binding_name, result.clone()).await;
986 Ok(result)
987 }
988
989 async fn load_kv(&self, binding_name: &str) -> Result<Arc<dyn Kv>> {
990 if let Some(cached) = self.get_cached::<Arc<dyn Kv>>("kv", binding_name).await {
991 return Ok(cached);
992 }
993
994 use alien_core::bindings::KvBinding;
995
996 let binding_json = self.bindings.get(binding_name).ok_or_else(|| {
997 AlienError::new(ErrorData::BindingConfigInvalid {
998 binding_name: binding_name.to_string(),
999 reason: "Binding not found".to_string(),
1000 })
1001 })?;
1002
1003 let binding: KvBinding = serde_json::from_value(binding_json.clone())
1004 .into_alien_error()
1005 .context(ErrorData::BindingConfigInvalid {
1006 binding_name: binding_name.to_string(),
1007 reason: "Failed to parse KV binding".to_string(),
1008 })?;
1009
1010 let result: Arc<dyn Kv> = match binding {
1011 #[cfg(feature = "aws")]
1012 KvBinding::Dynamodb(config) => {
1013 use crate::providers::kv::aws_dynamodb::AwsDynamodbKv;
1014
1015 let table_name = config
1016 .table_name
1017 .into_value(binding_name, "table_name")
1018 .context(ErrorData::BindingConfigInvalid {
1019 binding_name: binding_name.to_string(),
1020 reason: "Failed to extract table_name from DynamoDB binding".to_string(),
1021 })?;
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
1030 let credentials =
1031 alien_aws_clients::AwsCredentialProvider::from_config(aws_config.clone())
1032 .await
1033 .context(ErrorData::ClientConfigInvalid {
1034 platform: Platform::Aws,
1035 message: "Failed to create AWS credential provider".to_string(),
1036 })?;
1037 let dynamodb_client = alien_aws_clients::dynamodb::DynamoDbClient::new(
1038 crate::http_client::create_http_client(),
1039 credentials,
1040 );
1041 let kv_impl = AwsDynamodbKv::new(table_name, dynamodb_client);
1042 let kv: Arc<dyn Kv> = Arc::new(kv_impl);
1043 Ok(kv)
1044 }
1045 #[cfg(not(feature = "aws"))]
1046 KvBinding::Dynamodb(_) => Err(AlienError::new(ErrorData::FeatureNotEnabled {
1047 feature: "aws".to_string(),
1048 })),
1049
1050 #[cfg(feature = "gcp")]
1051 KvBinding::Firestore(config) => {
1052 use crate::providers::kv::gcp_firestore::GcpFirestoreKv;
1053 use alien_gcp_clients::firestore::FirestoreClient;
1054
1055 let gcp_config = self.client_config.gcp_config().ok_or_else(|| {
1056 AlienError::new(ErrorData::ClientConfigInvalid {
1057 platform: Platform::Gcp,
1058 message: "GCP config not available".to_string(),
1059 })
1060 })?;
1061
1062 let client = FirestoreClient::new(
1063 crate::http_client::create_http_client(),
1064 gcp_config.clone(),
1065 );
1066
1067 let project_id = config
1068 .project_id
1069 .into_value(binding_name, "project_id")
1070 .context(ErrorData::BindingConfigInvalid {
1071 binding_name: binding_name.to_string(),
1072 reason: "Failed to extract project_id from Firestore binding".to_string(),
1073 })?;
1074
1075 let database_id = config
1076 .database_id
1077 .into_value(binding_name, "database_id")
1078 .context(ErrorData::BindingConfigInvalid {
1079 binding_name: binding_name.to_string(),
1080 reason: "Failed to extract database_id from Firestore binding".to_string(),
1081 })?;
1082
1083 let collection_name = config
1084 .collection_name
1085 .into_value(binding_name, "collection_name")
1086 .context(ErrorData::BindingConfigInvalid {
1087 binding_name: binding_name.to_string(),
1088 reason: "Failed to extract collection_name from Firestore binding"
1089 .to_string(),
1090 })?;
1091
1092 let kv: Arc<dyn Kv> = Arc::new(GcpFirestoreKv::new(
1093 client,
1094 project_id,
1095 database_id,
1096 collection_name,
1097 )?);
1098 Ok(kv)
1099 }
1100 #[cfg(not(feature = "gcp"))]
1101 KvBinding::Firestore(_) => Err(AlienError::new(ErrorData::FeatureNotEnabled {
1102 feature: "gcp".to_string(),
1103 })),
1104
1105 #[cfg(feature = "azure")]
1106 KvBinding::TableStorage(config) => {
1107 use crate::providers::kv::azure_table_storage::AzureTableStorageKv;
1108 use alien_azure_clients::tables::AzureTableStorageClient;
1109 use alien_azure_clients::AzureTokenCache;
1110
1111 let azure_config = self.client_config.azure_config().ok_or_else(|| {
1112 AlienError::new(ErrorData::ClientConfigInvalid {
1113 platform: Platform::Azure,
1114 message: "Azure config not available".to_string(),
1115 })
1116 })?;
1117
1118 let resource_group_name = config
1119 .resource_group_name
1120 .into_value(binding_name, "resource_group_name")
1121 .context(ErrorData::BindingConfigInvalid {
1122 binding_name: binding_name.to_string(),
1123 reason: "Failed to extract resource_group_name from TableStorage binding"
1124 .to_string(),
1125 })?;
1126
1127 let account_name = config
1128 .account_name
1129 .into_value(binding_name, "account_name")
1130 .context(ErrorData::BindingConfigInvalid {
1131 binding_name: binding_name.to_string(),
1132 reason: "Failed to extract account_name from TableStorage binding"
1133 .to_string(),
1134 })?;
1135
1136 let table_name = config
1137 .table_name
1138 .into_value(binding_name, "table_name")
1139 .context(ErrorData::BindingConfigInvalid {
1140 binding_name: binding_name.to_string(),
1141 reason: "Failed to extract table_name from TableStorage binding"
1142 .to_string(),
1143 })?;
1144
1145 let client = AzureTableStorageClient::new(
1146 crate::http_client::create_http_client(),
1147 AzureTokenCache::new(azure_config.clone()),
1148 );
1149
1150 let kv_impl =
1151 AzureTableStorageKv::new(client, resource_group_name, account_name, table_name);
1152 let kv: Arc<dyn Kv> = Arc::new(kv_impl);
1153 Ok(kv)
1154 }
1155 #[cfg(not(feature = "azure"))]
1156 KvBinding::TableStorage(_) => Err(AlienError::new(ErrorData::FeatureNotEnabled {
1157 feature: "azure".to_string(),
1158 })),
1159
1160 #[cfg(feature = "local")]
1161 KvBinding::Local(local_binding) => {
1162 use crate::providers::kv::local::LocalKv;
1163 use std::path::PathBuf;
1164
1165 let data_dir = PathBuf::from(
1167 local_binding
1168 .data_dir
1169 .into_value(binding_name, "data_dir")
1170 .context(ErrorData::BindingConfigInvalid {
1171 binding_name: binding_name.to_string(),
1172 reason: "Failed to extract data_dir from Local binding".to_string(),
1173 })?,
1174 );
1175
1176 let kv_impl = LocalKv::new(data_dir).await?;
1178
1179 let kv: Arc<dyn Kv> = Arc::new(kv_impl);
1180 Ok(kv)
1181 }
1182 #[cfg(not(feature = "local"))]
1183 KvBinding::Local { .. } => Err(AlienError::new(ErrorData::FeatureNotEnabled {
1184 feature: "local".to_string(),
1185 })),
1186
1187 KvBinding::Redis(_) => Err(AlienError::new(ErrorData::NotImplemented {
1188 operation: "Redis KV binding".to_string(),
1189 reason: "Redis KV provider is not yet implemented".to_string(),
1190 })),
1191 }?;
1192
1193 self.put_cache("kv", binding_name, result.clone()).await;
1194 Ok(result)
1195 }
1196
1197 async fn load_queue(&self, binding_name: &str) -> Result<Arc<dyn Queue>> {
1198 if let Some(cached) = self
1199 .get_cached::<Arc<dyn Queue>>("queue", binding_name)
1200 .await
1201 {
1202 return Ok(cached);
1203 }
1204
1205 use alien_core::bindings::QueueBinding;
1206
1207 let binding_json = self.bindings.get(binding_name).ok_or_else(|| {
1208 AlienError::new(ErrorData::BindingConfigInvalid {
1209 binding_name: binding_name.to_string(),
1210 reason: "Binding not found".to_string(),
1211 })
1212 })?;
1213
1214 let binding: QueueBinding = serde_json::from_value(binding_json.clone())
1215 .into_alien_error()
1216 .context(ErrorData::BindingConfigInvalid {
1217 binding_name: binding_name.to_string(),
1218 reason: "Failed to parse Queue binding".to_string(),
1219 })?;
1220
1221 let result: Arc<dyn Queue> = match binding {
1222 #[cfg(feature = "aws")]
1223 QueueBinding::Sqs(config) => {
1224 use crate::providers::queue::aws_sqs::AwsSqsQueue;
1225
1226 let queue_url = config
1227 .queue_url
1228 .into_value(binding_name, "queue_url")
1229 .context(ErrorData::BindingConfigInvalid {
1230 binding_name: binding_name.to_string(),
1231 reason: "Failed to extract queue_url from SQS binding".to_string(),
1232 })?;
1233
1234 let aws_config = self.client_config.aws_config().ok_or_else(|| {
1235 AlienError::new(ErrorData::ClientConfigInvalid {
1236 platform: Platform::Aws,
1237 message: "AWS config not available".to_string(),
1238 })
1239 })?;
1240 let credentials =
1241 alien_aws_clients::AwsCredentialProvider::from_config(aws_config.clone())
1242 .await
1243 .context(ErrorData::ClientConfigInvalid {
1244 platform: Platform::Aws,
1245 message: "Failed to create AWS credential provider".to_string(),
1246 })?;
1247 let client = alien_aws_clients::sqs::SqsClient::new(
1248 crate::http_client::create_http_client(),
1249 credentials,
1250 );
1251 let q: Arc<dyn Queue> = Arc::new(AwsSqsQueue::new(queue_url, client));
1252 Ok(q)
1253 }
1254 #[cfg(not(feature = "aws"))]
1255 QueueBinding::Sqs(_) => Err(AlienError::new(ErrorData::FeatureNotEnabled {
1256 feature: "aws".to_string(),
1257 })),
1258
1259 #[cfg(feature = "gcp")]
1260 QueueBinding::Pubsub(config) => {
1261 use crate::providers::queue::gcp_pubsub::GcpPubSubQueue;
1262 let topic_name = config.topic.into_value(binding_name, "topic").context(
1263 ErrorData::BindingConfigInvalid {
1264 binding_name: binding_name.to_string(),
1265 reason: "Failed to extract topic".to_string(),
1266 },
1267 )?;
1268 let subscription_name = config
1269 .subscription
1270 .into_value(binding_name, "subscription")
1271 .context(ErrorData::BindingConfigInvalid {
1272 binding_name: binding_name.to_string(),
1273 reason: "Failed to extract subscription".to_string(),
1274 })?;
1275 let gcp_config = self.client_config.gcp_config().ok_or_else(|| {
1276 AlienError::new(ErrorData::ClientConfigInvalid {
1277 platform: Platform::Gcp,
1278 message: "GCP config not available".to_string(),
1279 })
1280 })?;
1281
1282 let topic = if let Some(short) =
1284 topic_name.strip_prefix(&format!("projects/{}/topics/", gcp_config.project_id))
1285 {
1286 short.to_string()
1287 } else {
1288 topic_name
1289 };
1290 let subscription = if let Some(short) = subscription_name.strip_prefix(&format!(
1291 "projects/{}/subscriptions/",
1292 gcp_config.project_id
1293 )) {
1294 short.to_string()
1295 } else {
1296 subscription_name
1297 };
1298
1299 let q: Arc<dyn Queue> =
1300 Arc::new(GcpPubSubQueue::new(topic, subscription, gcp_config.clone()).await?);
1301 Ok(q)
1302 }
1303 #[cfg(not(feature = "gcp"))]
1304 QueueBinding::Pubsub(_) => Err(AlienError::new(ErrorData::FeatureNotEnabled {
1305 feature: "gcp".to_string(),
1306 })),
1307
1308 #[cfg(feature = "azure")]
1309 QueueBinding::Servicebus(config) => {
1310 use crate::providers::queue::azure_service_bus::AzureServiceBusQueue;
1311 let namespace = config
1312 .namespace
1313 .into_value(binding_name, "namespace")
1314 .context(ErrorData::BindingConfigInvalid {
1315 binding_name: binding_name.to_string(),
1316 reason: "Failed to extract namespace".to_string(),
1317 })?;
1318 let queue_name = config
1319 .queue_name
1320 .into_value(binding_name, "queue_name")
1321 .context(ErrorData::BindingConfigInvalid {
1322 binding_name: binding_name.to_string(),
1323 reason: "Failed to extract queue_name".to_string(),
1324 })?;
1325 let azure_config = self.client_config.azure_config().ok_or_else(|| {
1326 AlienError::new(ErrorData::ClientConfigInvalid {
1327 platform: Platform::Azure,
1328 message: "Azure config not available".to_string(),
1329 })
1330 })?;
1331 let q: Arc<dyn Queue> = Arc::new(
1332 AzureServiceBusQueue::new(namespace, queue_name, azure_config.clone()).await?,
1333 );
1334 Ok(q)
1335 }
1336 #[cfg(not(feature = "azure"))]
1337 QueueBinding::Servicebus(_) => Err(AlienError::new(ErrorData::FeatureNotEnabled {
1338 feature: "azure".to_string(),
1339 })),
1340
1341 #[cfg(feature = "local")]
1342 QueueBinding::Local(config) => {
1343 use crate::providers::queue::local::LocalQueue;
1344
1345 let queue = LocalQueue::from_binding(config).await?;
1346 let q: Arc<dyn Queue> = Arc::new(queue);
1347 Ok(q)
1348 }
1349 #[cfg(not(feature = "local"))]
1350 QueueBinding::Local(_) => Err(AlienError::new(ErrorData::FeatureNotEnabled {
1351 feature: "local".to_string(),
1352 })),
1353 }?;
1354
1355 self.put_cache("queue", binding_name, result.clone()).await;
1356 Ok(result)
1357 }
1358
1359 async fn load_worker(&self, binding_name: &str) -> Result<Arc<dyn Worker>> {
1360 use alien_core::bindings::WorkerBinding;
1361
1362 let binding_json = self.bindings.get(binding_name).ok_or_else(|| {
1363 AlienError::new(ErrorData::BindingConfigInvalid {
1364 binding_name: binding_name.to_string(),
1365 reason: "Binding not found".to_string(),
1366 })
1367 })?;
1368
1369 let binding: WorkerBinding = serde_json::from_value(binding_json.clone())
1370 .into_alien_error()
1371 .context(ErrorData::BindingConfigInvalid {
1372 binding_name: binding_name.to_string(),
1373 reason: "Failed to parse worker binding".to_string(),
1374 })?;
1375
1376 match binding {
1377 #[cfg(feature = "aws")]
1378 WorkerBinding::Lambda(lambda_binding) => {
1379 use crate::providers::worker::LambdaWorker;
1380
1381 let aws_config = self.client_config.aws_config().ok_or_else(|| {
1382 AlienError::new(ErrorData::ClientConfigInvalid {
1383 platform: Platform::Aws,
1384 message: "AWS config not available".to_string(),
1385 })
1386 })?;
1387 let credentials =
1388 alien_aws_clients::AwsCredentialProvider::from_config(aws_config.clone())
1389 .await
1390 .context(ErrorData::ClientConfigInvalid {
1391 platform: Platform::Aws,
1392 message: "Failed to create AWS credential provider".to_string(),
1393 })?;
1394 let client = crate::http_client::create_http_client();
1395
1396 let function_impl = LambdaWorker::new(client, credentials, lambda_binding);
1397 let function: Arc<dyn Worker> = Arc::new(function_impl);
1398 Ok(function)
1399 }
1400 #[cfg(not(feature = "aws"))]
1401 WorkerBinding::Lambda(_) => Err(AlienError::new(ErrorData::FeatureNotEnabled {
1402 feature: "aws".to_string(),
1403 })),
1404
1405 #[cfg(feature = "gcp")]
1406 WorkerBinding::CloudRun(cloudrun_binding) => {
1407 use crate::providers::worker::CloudRunWorker;
1408
1409 let gcp_config = self.client_config.gcp_config().ok_or_else(|| {
1410 AlienError::new(ErrorData::ClientConfigInvalid {
1411 platform: Platform::Gcp,
1412 message: "GCP config not available".to_string(),
1413 })
1414 })?;
1415 let client = crate::http_client::create_http_client();
1416
1417 let function_impl =
1418 CloudRunWorker::new(client, gcp_config.clone(), cloudrun_binding);
1419 let function: Arc<dyn Worker> = Arc::new(function_impl);
1420 Ok(function)
1421 }
1422 #[cfg(not(feature = "gcp"))]
1423 WorkerBinding::CloudRun(_) => Err(AlienError::new(ErrorData::FeatureNotEnabled {
1424 feature: "gcp".to_string(),
1425 })),
1426
1427 #[cfg(feature = "azure")]
1428 WorkerBinding::ContainerApp(container_app_binding) => {
1429 use crate::providers::worker::ContainerAppWorker;
1430
1431 let azure_config = self.client_config.azure_config().ok_or_else(|| {
1432 AlienError::new(ErrorData::ClientConfigInvalid {
1433 platform: Platform::Azure,
1434 message: "Azure config not available".to_string(),
1435 })
1436 })?;
1437 let client = crate::http_client::create_http_client();
1438
1439 let function_impl =
1440 ContainerAppWorker::new(client, azure_config.clone(), container_app_binding);
1441 let function: Arc<dyn Worker> = Arc::new(function_impl);
1442 Ok(function)
1443 }
1444 #[cfg(not(feature = "azure"))]
1445 WorkerBinding::ContainerApp(_) => Err(AlienError::new(ErrorData::FeatureNotEnabled {
1446 feature: "azure".to_string(),
1447 })),
1448
1449 #[cfg(feature = "local")]
1450 WorkerBinding::Local(local_binding) => {
1451 use crate::providers::worker::LocalWorker;
1452
1453 let function_impl = LocalWorker::new(local_binding);
1454 let function: Arc<dyn Worker> = Arc::new(function_impl);
1455 Ok(function)
1456 }
1457 #[cfg(not(feature = "local"))]
1458 WorkerBinding::Local(_) => Err(AlienError::new(ErrorData::FeatureNotEnabled {
1459 feature: "local".to_string(),
1460 })),
1461
1462 #[cfg(feature = "kubernetes")]
1463 WorkerBinding::Kubernetes(kubernetes_binding) => {
1464 use crate::providers::worker::KubernetesWorker;
1465
1466 let function_impl =
1467 KubernetesWorker::new(binding_name.to_string(), kubernetes_binding)?;
1468 let function: Arc<dyn Worker> = Arc::new(function_impl);
1469 Ok(function)
1470 }
1471 #[cfg(not(feature = "kubernetes"))]
1472 WorkerBinding::Kubernetes(_) => Err(AlienError::new(ErrorData::FeatureNotEnabled {
1473 feature: "kubernetes".to_string(),
1474 })),
1475 }
1476 }
1477
1478 async fn load_container(
1479 &self,
1480 binding_name: &str,
1481 ) -> Result<Arc<dyn crate::traits::Container>> {
1482 use alien_core::bindings::ContainerBinding;
1483
1484 let binding_json = self.bindings.get(binding_name).ok_or_else(|| {
1485 AlienError::new(ErrorData::BindingConfigInvalid {
1486 binding_name: binding_name.to_string(),
1487 reason: "Binding not found".to_string(),
1488 })
1489 })?;
1490
1491 let binding: ContainerBinding = serde_json::from_value(binding_json.clone())
1492 .into_alien_error()
1493 .context(ErrorData::BindingConfigInvalid {
1494 binding_name: binding_name.to_string(),
1495 reason: "Failed to parse container binding".to_string(),
1496 })?;
1497
1498 match binding {
1499 ContainerBinding::Horizon(horizon_binding) => {
1500 use crate::providers::container::HorizonContainer;
1501
1502 let container_impl = HorizonContainer::new(horizon_binding)?;
1503 let container: Arc<dyn crate::traits::Container> = Arc::new(container_impl);
1504 Ok(container)
1505 }
1506
1507 #[cfg(feature = "local")]
1508 ContainerBinding::Local(local_binding) => {
1509 use crate::providers::container::LocalContainer;
1510
1511 let container_impl = LocalContainer::new(local_binding)?;
1512 let container: Arc<dyn crate::traits::Container> = Arc::new(container_impl);
1513 Ok(container)
1514 }
1515 #[cfg(not(feature = "local"))]
1516 ContainerBinding::Local(_) => Err(AlienError::new(ErrorData::FeatureNotEnabled {
1517 feature: "local".to_string(),
1518 })),
1519
1520 #[cfg(feature = "kubernetes")]
1521 ContainerBinding::Kubernetes(kubernetes_binding) => {
1522 use crate::providers::container::KubernetesContainer;
1523
1524 let container_impl =
1525 KubernetesContainer::new(binding_name.to_string(), kubernetes_binding)?;
1526 let container: Arc<dyn crate::traits::Container> = Arc::new(container_impl);
1527 Ok(container)
1528 }
1529 #[cfg(not(feature = "kubernetes"))]
1530 ContainerBinding::Kubernetes(_) => Err(AlienError::new(ErrorData::FeatureNotEnabled {
1531 feature: "kubernetes".to_string(),
1532 })),
1533 }
1534 }
1535
1536 async fn load_service_account(
1537 &self,
1538 binding_name: &str,
1539 ) -> Result<Arc<dyn crate::traits::ServiceAccount>> {
1540 use alien_core::bindings::ServiceAccountBinding;
1541
1542 let binding_json = self.bindings.get(binding_name).ok_or_else(|| {
1543 AlienError::new(ErrorData::BindingConfigInvalid {
1544 binding_name: binding_name.to_string(),
1545 reason: "Binding not found".to_string(),
1546 })
1547 })?;
1548
1549 let binding: ServiceAccountBinding = serde_json::from_value(binding_json.clone())
1550 .into_alien_error()
1551 .context(ErrorData::BindingConfigInvalid {
1552 binding_name: binding_name.to_string(),
1553 reason: "Failed to parse service account binding".to_string(),
1554 })?;
1555
1556 match binding {
1557 #[cfg(feature = "aws")]
1558 ServiceAccountBinding::AwsIam(aws_binding) => {
1559 use crate::providers::service_account::aws_iam::AwsIamServiceAccount;
1560
1561 let aws_config = self.client_config.aws_config().ok_or_else(|| {
1562 AlienError::new(ErrorData::ClientConfigInvalid {
1563 platform: Platform::Aws,
1564 message: "AWS config not available".to_string(),
1565 })
1566 })?;
1567 let client = crate::http_client::create_http_client();
1568
1569 let service_account_impl =
1570 AwsIamServiceAccount::new(client, aws_config.clone(), aws_binding);
1571 let service_account: Arc<dyn crate::traits::ServiceAccount> =
1572 Arc::new(service_account_impl);
1573 Ok(service_account)
1574 }
1575 #[cfg(not(feature = "aws"))]
1576 ServiceAccountBinding::AwsIam(_) => {
1577 Err(AlienError::new(ErrorData::FeatureNotEnabled {
1578 feature: "aws".to_string(),
1579 }))
1580 }
1581
1582 #[cfg(feature = "gcp")]
1583 ServiceAccountBinding::GcpServiceAccount(gcp_binding) => {
1584 use crate::providers::service_account::gcp_service_account::GcpServiceAccount;
1585
1586 let gcp_config = self.client_config.gcp_config().ok_or_else(|| {
1587 AlienError::new(ErrorData::ClientConfigInvalid {
1588 platform: Platform::Gcp,
1589 message: "GCP config not available".to_string(),
1590 })
1591 })?;
1592 let client = crate::http_client::create_http_client();
1593
1594 let service_account_impl =
1595 GcpServiceAccount::new(client, gcp_config.clone(), gcp_binding);
1596 let service_account: Arc<dyn crate::traits::ServiceAccount> =
1597 Arc::new(service_account_impl);
1598 Ok(service_account)
1599 }
1600 #[cfg(not(feature = "gcp"))]
1601 ServiceAccountBinding::GcpServiceAccount(_) => {
1602 Err(AlienError::new(ErrorData::FeatureNotEnabled {
1603 feature: "gcp".to_string(),
1604 }))
1605 }
1606
1607 #[cfg(feature = "azure")]
1608 ServiceAccountBinding::AzureManagedIdentity(azure_binding) => {
1609 use crate::providers::service_account::azure_managed_identity::AzureManagedIdentityServiceAccount;
1610
1611 let azure_config = self.client_config.azure_config().ok_or_else(|| {
1612 AlienError::new(ErrorData::ClientConfigInvalid {
1613 platform: Platform::Azure,
1614 message: "Azure config not available".to_string(),
1615 })
1616 })?;
1617
1618 let service_account_impl =
1619 AzureManagedIdentityServiceAccount::new(azure_config.clone(), azure_binding);
1620 let service_account: Arc<dyn crate::traits::ServiceAccount> =
1621 Arc::new(service_account_impl);
1622 Ok(service_account)
1623 }
1624 #[cfg(not(feature = "azure"))]
1625 ServiceAccountBinding::AzureManagedIdentity(_) => {
1626 Err(AlienError::new(ErrorData::FeatureNotEnabled {
1627 feature: "azure".to_string(),
1628 }))
1629 }
1630 }
1631 }
1632}
1633
1634#[cfg(test)]
1635mod tests {
1636 use super::*;
1637 use alien_core::ENV_ALIEN_DEPLOYMENT_TYPE;
1638
1639 fn kubernetes_aws_env() -> HashMap<String, String> {
1640 HashMap::from([
1641 (
1642 ENV_ALIEN_DEPLOYMENT_TYPE.to_string(),
1643 Platform::Kubernetes.as_str().to_string(),
1644 ),
1645 (
1646 ENV_ALIEN_BASE_PLATFORM.to_string(),
1647 Platform::Aws.as_str().to_string(),
1648 ),
1649 (
1650 "KUBERNETES_SERVICE_HOST".to_string(),
1651 "10.0.0.1".to_string(),
1652 ),
1653 ("KUBERNETES_SERVICE_PORT".to_string(), "443".to_string()),
1654 ("AWS_REGION".to_string(), "us-east-1".to_string()),
1655 ("AWS_ACCOUNT_ID".to_string(), "123456789012".to_string()),
1656 ("AWS_ACCESS_KEY_ID".to_string(), "test".to_string()),
1657 ("AWS_SECRET_ACCESS_KEY".to_string(), "test".to_string()),
1658 ])
1659 }
1660
1661 fn kubernetes_azure_env() -> HashMap<String, String> {
1662 HashMap::from([
1663 (
1664 ENV_ALIEN_DEPLOYMENT_TYPE.to_string(),
1665 Platform::Kubernetes.as_str().to_string(),
1666 ),
1667 (
1668 ENV_ALIEN_BASE_PLATFORM.to_string(),
1669 Platform::Azure.as_str().to_string(),
1670 ),
1671 (
1672 "KUBERNETES_SERVICE_HOST".to_string(),
1673 "10.0.0.1".to_string(),
1674 ),
1675 ("KUBERNETES_SERVICE_PORT".to_string(), "443".to_string()),
1676 (
1677 "AZURE_SUBSCRIPTION_ID".to_string(),
1678 "00000000-0000-0000-0000-000000000000".to_string(),
1679 ),
1680 (
1681 "AZURE_TENANT_ID".to_string(),
1682 "11111111-1111-1111-1111-111111111111".to_string(),
1683 ),
1684 ("AZURE_REGION".to_string(), "eastus".to_string()),
1685 (
1686 "AZURE_CLIENT_ID".to_string(),
1687 "22222222-2222-2222-2222-222222222222".to_string(),
1688 ),
1689 (
1690 "AZURE_FEDERATED_TOKEN_FILE".to_string(),
1691 "/var/run/secrets/azure/tokens/azure-identity-token".to_string(),
1692 ),
1693 (
1694 "AZURE_AUTHORITY_HOST".to_string(),
1695 "https://login.microsoftonline.com/".to_string(),
1696 ),
1697 ])
1698 }
1699
1700 #[tokio::test]
1701 async fn from_env_builds_kubernetes_cloud_config_when_base_platform_is_set() {
1702 let provider = BindingsProvider::from_env(kubernetes_aws_env())
1703 .await
1704 .unwrap();
1705
1706 assert!(provider.client_config.kubernetes_config().is_some());
1707 assert!(provider.client_config.aws_config().is_some());
1708 assert!(matches!(
1709 provider.client_config,
1710 ClientConfig::KubernetesCloud { .. }
1711 ));
1712 }
1713
1714 #[tokio::test]
1715 async fn from_env_builds_kubernetes_cloud_config_for_azure_workload_identity() {
1716 let provider = BindingsProvider::from_env(kubernetes_azure_env())
1717 .await
1718 .unwrap();
1719
1720 assert!(provider.client_config.kubernetes_config().is_some());
1721 assert!(provider.client_config.azure_config().is_some());
1722 assert!(matches!(
1723 provider.client_config,
1724 ClientConfig::KubernetesCloud { .. }
1725 ));
1726 }
1727
1728 #[tokio::test]
1729 async fn from_env_rejects_non_cloud_kubernetes_base_platform() {
1730 let mut env = kubernetes_aws_env();
1731 env.insert(
1732 ENV_ALIEN_BASE_PLATFORM.to_string(),
1733 Platform::Kubernetes.as_str().to_string(),
1734 );
1735
1736 let error = BindingsProvider::from_env(env).await.unwrap_err();
1737
1738 assert!(error.to_string().contains(ENV_ALIEN_BASE_PLATFORM));
1739 }
1740}
1741
1742#[cfg(feature = "platform-sdk")]
1744mod conversions {
1745 use super::*;
1746 use serde::Serialize;
1747
1748 pub fn convert_stack_state<T: Serialize>(sdk_stack_state: &T) -> Result<StackState> {
1751 let stack_state: StackState = serde_json::from_value(
1753 serde_json::to_value(sdk_stack_state)
1754 .into_alien_error()
1755 .context(ErrorData::BindingConfigInvalid {
1756 binding_name: "stack_state".to_string(),
1757 reason: "Failed to serialize SDK stack state".to_string(),
1758 })?,
1759 )
1760 .into_alien_error()
1761 .context(ErrorData::BindingConfigInvalid {
1762 binding_name: "stack_state".to_string(),
1763 reason: "Failed to parse stack state".to_string(),
1764 })?;
1765
1766 Ok(stack_state)
1767 }
1768}