Skip to main content

alien_core/
external_bindings.rs

1//! External bindings for pre-existing infrastructure services.
2//!
3//! External bindings allow using existing infrastructure (MinIO, Kafka, Redis, etc.)
4//! instead of having Alien provision cloud resources. This is required for Kubernetes
5//! platform deployments and optional for cloud platforms (to override specific resources).
6
7use std::collections::HashMap;
8
9use alien_error::AlienError;
10use serde::{Deserialize, Serialize};
11
12use crate::bindings::{
13    ArtifactRegistryBinding, KvBinding, QueueBinding, StorageBinding, VaultBinding,
14};
15use crate::error::ErrorData;
16use crate::Resource;
17
18/// Represents a binding to pre-existing infrastructure.
19///
20/// The binding type must match the resource type it's applied to.
21/// Validated at runtime by the executor.
22#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
23#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
24#[serde(tag = "type", rename_all = "snake_case")]
25pub enum ExternalBinding {
26    /// External storage binding (S3-compatible, GCS, Blob Storage)
27    Storage(StorageBinding),
28    /// External queue binding (Kafka, SQS, etc.)
29    Queue(QueueBinding),
30    /// External KV binding (Redis, etc.)
31    Kv(KvBinding),
32    /// External artifact registry binding (OCI registry)
33    ArtifactRegistry(ArtifactRegistryBinding),
34    /// External vault binding (HashiCorp Vault, etc.)
35    Vault(VaultBinding),
36}
37
38/// Map from resource ID to external binding.
39///
40/// Validated at runtime: binding type must match resource type.
41#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)]
42#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
43#[serde(transparent)]
44pub struct ExternalBindings(pub HashMap<String, ExternalBinding>);
45
46impl ExternalBindings {
47    /// Creates an empty ExternalBindings map.
48    pub fn new() -> Self {
49        Self(HashMap::new())
50    }
51
52    /// Checks if a binding exists for the given resource ID.
53    pub fn has(&self, resource_id: &str) -> bool {
54        self.0.contains_key(resource_id)
55    }
56
57    /// Gets an external binding by resource ID.
58    pub fn get(&self, resource_id: &str) -> Option<&ExternalBinding> {
59        self.0.get(resource_id)
60    }
61
62    /// Gets a storage binding for the given resource ID.
63    /// Returns an error if the binding exists but is not a Storage type.
64    pub fn get_storage(&self, id: &str) -> crate::error::Result<Option<&StorageBinding>> {
65        match self.0.get(id) {
66            Some(ExternalBinding::Storage(b)) => Ok(Some(b)),
67            Some(other) => Err(AlienError::new(ErrorData::ExternalBindingTypeMismatch {
68                resource_id: id.to_string(),
69                expected: "storage".to_string(),
70                actual: other.binding_type().to_string(),
71            })),
72            None => Ok(None),
73        }
74    }
75
76    /// Gets a queue binding for the given resource ID.
77    /// Returns an error if the binding exists but is not a Queue type.
78    pub fn get_queue(&self, id: &str) -> crate::error::Result<Option<&QueueBinding>> {
79        match self.0.get(id) {
80            Some(ExternalBinding::Queue(b)) => Ok(Some(b)),
81            Some(other) => Err(AlienError::new(ErrorData::ExternalBindingTypeMismatch {
82                resource_id: id.to_string(),
83                expected: "queue".to_string(),
84                actual: other.binding_type().to_string(),
85            })),
86            None => Ok(None),
87        }
88    }
89
90    /// Gets a KV binding for the given resource ID.
91    /// Returns an error if the binding exists but is not a Kv type.
92    pub fn get_kv(&self, id: &str) -> crate::error::Result<Option<&KvBinding>> {
93        match self.0.get(id) {
94            Some(ExternalBinding::Kv(b)) => Ok(Some(b)),
95            Some(other) => Err(AlienError::new(ErrorData::ExternalBindingTypeMismatch {
96                resource_id: id.to_string(),
97                expected: "kv".to_string(),
98                actual: other.binding_type().to_string(),
99            })),
100            None => Ok(None),
101        }
102    }
103
104    /// Gets an artifact registry binding for the given resource ID.
105    /// Returns an error if the binding exists but is not an ArtifactRegistry type.
106    pub fn get_artifact_registry(
107        &self,
108        id: &str,
109    ) -> crate::error::Result<Option<&ArtifactRegistryBinding>> {
110        match self.0.get(id) {
111            Some(ExternalBinding::ArtifactRegistry(b)) => Ok(Some(b)),
112            Some(other) => Err(AlienError::new(ErrorData::ExternalBindingTypeMismatch {
113                resource_id: id.to_string(),
114                expected: "artifact_registry".to_string(),
115                actual: other.binding_type().to_string(),
116            })),
117            None => Ok(None),
118        }
119    }
120
121    /// Gets a vault binding for the given resource ID.
122    /// Returns an error if the binding exists but is not a Vault type.
123    pub fn get_vault(&self, id: &str) -> crate::error::Result<Option<&VaultBinding>> {
124        match self.0.get(id) {
125            Some(ExternalBinding::Vault(b)) => Ok(Some(b)),
126            Some(other) => Err(AlienError::new(ErrorData::ExternalBindingTypeMismatch {
127                resource_id: id.to_string(),
128                expected: "vault".to_string(),
129                actual: other.binding_type().to_string(),
130            })),
131            None => Ok(None),
132        }
133    }
134
135    /// Inserts an external binding for a resource.
136    pub fn insert(&mut self, resource_id: impl Into<String>, binding: ExternalBinding) {
137        self.0.insert(resource_id.into(), binding);
138    }
139}
140
141impl ExternalBinding {
142    /// Returns the type name of this binding variant.
143    pub fn binding_type(&self) -> &'static str {
144        match self {
145            ExternalBinding::Storage(_) => "storage",
146            ExternalBinding::Queue(_) => "queue",
147            ExternalBinding::Kv(_) => "kv",
148            ExternalBinding::ArtifactRegistry(_) => "artifact_registry",
149            ExternalBinding::Vault(_) => "vault",
150        }
151    }
152}
153
154/// Validates that an external binding type matches the resource type.
155pub fn validate_binding_type(
156    resource: &Resource,
157    binding: &ExternalBinding,
158) -> crate::error::Result<()> {
159    let resource_type = resource.resource_type();
160    let resource_type_str = resource_type.as_ref();
161
162    let valid = match (resource_type_str, binding) {
163        ("storage", ExternalBinding::Storage(_)) => true,
164        ("queue", ExternalBinding::Queue(_)) => true,
165        ("kv", ExternalBinding::Kv(_)) => true,
166        ("artifact_registry", ExternalBinding::ArtifactRegistry(_)) => true,
167        ("vault", ExternalBinding::Vault(_)) => true,
168        _ => false,
169    };
170
171    if !valid {
172        return Err(AlienError::new(ErrorData::ExternalBindingTypeMismatch {
173            resource_id: resource.id().to_string(),
174            expected: resource_type_str.to_string(),
175            actual: binding.binding_type().to_string(),
176        }));
177    }
178    Ok(())
179}
180
181#[cfg(test)]
182mod tests {
183    use super::*;
184    use crate::bindings::{KvBinding, StorageBinding};
185
186    #[test]
187    fn test_external_bindings_storage() {
188        let mut bindings = ExternalBindings::new();
189        bindings.insert(
190            "data-storage",
191            ExternalBinding::Storage(StorageBinding::s3("my-bucket")),
192        );
193
194        assert!(bindings.has("data-storage"));
195        assert!(bindings.get_storage("data-storage").unwrap().is_some());
196        assert!(bindings.get_queue("data-storage").is_err()); // Wrong type
197    }
198
199    #[test]
200    fn test_external_bindings_kv() {
201        let mut bindings = ExternalBindings::new();
202        bindings.insert(
203            "cache",
204            ExternalBinding::Kv(KvBinding::redis("redis://localhost:6379")),
205        );
206
207        assert!(bindings.has("cache"));
208        assert!(bindings.get_kv("cache").unwrap().is_some());
209        assert!(bindings.get_storage("cache").is_err()); // Wrong type
210    }
211
212    #[test]
213    fn test_external_bindings_serialization() {
214        let mut bindings = ExternalBindings::new();
215        bindings.insert(
216            "data",
217            ExternalBinding::Storage(StorageBinding::s3("test-bucket")),
218        );
219
220        let json = serde_json::to_string(&bindings).unwrap();
221        let deserialized: ExternalBindings = serde_json::from_str(&json).unwrap();
222        assert_eq!(bindings, deserialized);
223    }
224}