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, BindingValue, ContainerAppsEnvironmentBinding, KvBinding,
14    QueueBinding, StorageBinding, VaultBinding,
15};
16use crate::error::ErrorData;
17use crate::resource::ResourceOutputs;
18use crate::resources::AzureContainerAppsEnvironmentOutputs;
19use crate::Resource;
20
21/// Represents a binding to pre-existing infrastructure.
22///
23/// The binding type must match the resource type it's applied to.
24/// Validated at runtime by the executor.
25#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
26#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
27#[serde(tag = "type", rename_all = "snake_case")]
28pub enum ExternalBinding {
29    /// External storage binding (S3-compatible, GCS, Blob Storage)
30    Storage(StorageBinding),
31    /// External queue binding (Kafka, SQS, etc.)
32    Queue(QueueBinding),
33    /// External KV binding (Redis, etc.)
34    Kv(KvBinding),
35    /// External artifact registry binding (OCI registry)
36    ArtifactRegistry(ArtifactRegistryBinding),
37    /// External vault binding (HashiCorp Vault, etc.)
38    Vault(VaultBinding),
39    /// External Azure Container Apps Environment binding (pre-existing environment)
40    ContainerAppsEnvironment(ContainerAppsEnvironmentBinding),
41}
42
43/// Map from resource ID to external binding.
44///
45/// Validated at runtime: binding type must match resource type.
46#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)]
47#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
48#[serde(transparent)]
49pub struct ExternalBindings(pub HashMap<String, ExternalBinding>);
50
51impl ExternalBindings {
52    /// Creates an empty ExternalBindings map.
53    pub fn new() -> Self {
54        Self(HashMap::new())
55    }
56
57    /// Returns true if there are no external bindings.
58    pub fn is_empty(&self) -> bool {
59        self.0.is_empty()
60    }
61
62    /// Checks if a binding exists for the given resource ID.
63    pub fn has(&self, resource_id: &str) -> bool {
64        self.0.contains_key(resource_id)
65    }
66
67    /// Gets an external binding by resource ID.
68    pub fn get(&self, resource_id: &str) -> Option<&ExternalBinding> {
69        self.0.get(resource_id)
70    }
71
72    /// Gets a storage binding for the given resource ID.
73    /// Returns an error if the binding exists but is not a Storage type.
74    pub fn get_storage(&self, id: &str) -> crate::error::Result<Option<&StorageBinding>> {
75        match self.0.get(id) {
76            Some(ExternalBinding::Storage(b)) => Ok(Some(b)),
77            Some(other) => Err(AlienError::new(ErrorData::ExternalBindingTypeMismatch {
78                resource_id: id.to_string(),
79                expected: "storage".to_string(),
80                actual: other.binding_type().to_string(),
81            })),
82            None => Ok(None),
83        }
84    }
85
86    /// Gets a queue binding for the given resource ID.
87    /// Returns an error if the binding exists but is not a Queue type.
88    pub fn get_queue(&self, id: &str) -> crate::error::Result<Option<&QueueBinding>> {
89        match self.0.get(id) {
90            Some(ExternalBinding::Queue(b)) => Ok(Some(b)),
91            Some(other) => Err(AlienError::new(ErrorData::ExternalBindingTypeMismatch {
92                resource_id: id.to_string(),
93                expected: "queue".to_string(),
94                actual: other.binding_type().to_string(),
95            })),
96            None => Ok(None),
97        }
98    }
99
100    /// Gets a KV binding for the given resource ID.
101    /// Returns an error if the binding exists but is not a Kv type.
102    pub fn get_kv(&self, id: &str) -> crate::error::Result<Option<&KvBinding>> {
103        match self.0.get(id) {
104            Some(ExternalBinding::Kv(b)) => Ok(Some(b)),
105            Some(other) => Err(AlienError::new(ErrorData::ExternalBindingTypeMismatch {
106                resource_id: id.to_string(),
107                expected: "kv".to_string(),
108                actual: other.binding_type().to_string(),
109            })),
110            None => Ok(None),
111        }
112    }
113
114    /// Gets an artifact registry binding for the given resource ID.
115    /// Returns an error if the binding exists but is not an ArtifactRegistry type.
116    pub fn get_artifact_registry(
117        &self,
118        id: &str,
119    ) -> crate::error::Result<Option<&ArtifactRegistryBinding>> {
120        match self.0.get(id) {
121            Some(ExternalBinding::ArtifactRegistry(b)) => Ok(Some(b)),
122            Some(other) => Err(AlienError::new(ErrorData::ExternalBindingTypeMismatch {
123                resource_id: id.to_string(),
124                expected: "artifact_registry".to_string(),
125                actual: other.binding_type().to_string(),
126            })),
127            None => Ok(None),
128        }
129    }
130
131    /// Gets a vault binding for the given resource ID.
132    /// Returns an error if the binding exists but is not a Vault type.
133    pub fn get_vault(&self, id: &str) -> crate::error::Result<Option<&VaultBinding>> {
134        match self.0.get(id) {
135            Some(ExternalBinding::Vault(b)) => Ok(Some(b)),
136            Some(other) => Err(AlienError::new(ErrorData::ExternalBindingTypeMismatch {
137                resource_id: id.to_string(),
138                expected: "vault".to_string(),
139                actual: other.binding_type().to_string(),
140            })),
141            None => Ok(None),
142        }
143    }
144
145    /// Gets a container apps environment binding for the given resource ID.
146    /// Returns an error if the binding exists but is not a ContainerAppsEnvironment type.
147    pub fn get_container_apps_environment(
148        &self,
149        id: &str,
150    ) -> crate::error::Result<Option<&ContainerAppsEnvironmentBinding>> {
151        match self.0.get(id) {
152            Some(ExternalBinding::ContainerAppsEnvironment(b)) => Ok(Some(b)),
153            Some(other) => Err(AlienError::new(ErrorData::ExternalBindingTypeMismatch {
154                resource_id: id.to_string(),
155                expected: "azure_container_apps_environment".to_string(),
156                actual: other.binding_type().to_string(),
157            })),
158            None => Ok(None),
159        }
160    }
161
162    /// Inserts an external binding for a resource.
163    pub fn insert(&mut self, resource_id: impl Into<String>, binding: ExternalBinding) {
164        self.0.insert(resource_id.into(), binding);
165    }
166}
167
168impl ExternalBinding {
169    /// Returns the type name of this binding variant.
170    pub fn binding_type(&self) -> &'static str {
171        match self {
172            ExternalBinding::Storage(_) => "storage",
173            ExternalBinding::Queue(_) => "queue",
174            ExternalBinding::Kv(_) => "kv",
175            ExternalBinding::ArtifactRegistry(_) => "artifact_registry",
176            ExternalBinding::Vault(_) => "vault",
177            ExternalBinding::ContainerAppsEnvironment(_) => "azure_container_apps_environment",
178        }
179    }
180
181    /// Converts this external binding into resource outputs that dependent resources
182    /// can read via `get_resource_outputs()`.
183    ///
184    /// Infrastructure bindings (Container Apps Environment) produce typed outputs so that
185    /// dependent resources like functions and builds can read the environment's name,
186    /// resource ID, and resource group. Application-level bindings (Storage, Queue, KV, etc.)
187    /// return `None` — they are consumed via `remote_binding_params` and environment variables
188    /// rather than `get_resource_outputs()`.
189    pub fn to_resource_outputs(&self) -> Option<ResourceOutputs> {
190        match self {
191            ExternalBinding::ContainerAppsEnvironment(binding) => {
192                // Extract concrete values from BindingValue wrappers.
193                // External bindings for pre-provisioned resources always use concrete values.
194                let environment_name = match &binding.environment_name {
195                    BindingValue::Value(v) => v.clone(),
196                    _ => return None,
197                };
198                let resource_id = match &binding.resource_id {
199                    BindingValue::Value(v) => v.clone(),
200                    _ => return None,
201                };
202                let resource_group_name = match &binding.resource_group_name {
203                    BindingValue::Value(v) => v.clone(),
204                    _ => return None,
205                };
206                let default_domain = match &binding.default_domain {
207                    BindingValue::Value(v) => v.clone(),
208                    _ => return None,
209                };
210                let static_ip = binding.static_ip.as_ref().and_then(|v| match v {
211                    BindingValue::Value(v) => Some(v.clone()),
212                    _ => None,
213                });
214
215                Some(ResourceOutputs::new(AzureContainerAppsEnvironmentOutputs {
216                    environment_name,
217                    resource_id,
218                    resource_group_name,
219                    default_domain,
220                    static_ip,
221                    custom_domain_verification_id: None,
222                }))
223            }
224            // Application-level bindings are consumed via remote_binding_params, not outputs
225            _ => None,
226        }
227    }
228}
229
230/// Validates that an external binding type matches the resource type.
231pub fn validate_binding_type(
232    resource: &Resource,
233    binding: &ExternalBinding,
234) -> crate::error::Result<()> {
235    let resource_type = resource.resource_type();
236    let resource_type_str = resource_type.as_ref();
237
238    let valid = match (resource_type_str, binding) {
239        ("storage", ExternalBinding::Storage(_)) => true,
240        ("queue", ExternalBinding::Queue(_)) => true,
241        ("kv", ExternalBinding::Kv(_)) => true,
242        ("artifact_registry", ExternalBinding::ArtifactRegistry(_)) => true,
243        ("vault", ExternalBinding::Vault(_)) => true,
244        ("azure_container_apps_environment", ExternalBinding::ContainerAppsEnvironment(_)) => true,
245        _ => false,
246    };
247
248    if !valid {
249        return Err(AlienError::new(ErrorData::ExternalBindingTypeMismatch {
250            resource_id: resource.id().to_string(),
251            expected: resource_type_str.to_string(),
252            actual: binding.binding_type().to_string(),
253        }));
254    }
255    Ok(())
256}
257
258#[cfg(test)]
259mod tests {
260    use super::*;
261    use crate::bindings::{KvBinding, StorageBinding};
262
263    #[test]
264    fn test_external_bindings_storage() {
265        let mut bindings = ExternalBindings::new();
266        bindings.insert(
267            "data-storage",
268            ExternalBinding::Storage(StorageBinding::s3("my-bucket")),
269        );
270
271        assert!(bindings.has("data-storage"));
272        assert!(bindings.get_storage("data-storage").unwrap().is_some());
273        assert!(bindings.get_queue("data-storage").is_err()); // Wrong type
274    }
275
276    #[test]
277    fn test_external_bindings_kv() {
278        let mut bindings = ExternalBindings::new();
279        bindings.insert(
280            "cache",
281            ExternalBinding::Kv(KvBinding::redis("redis://localhost:6379")),
282        );
283
284        assert!(bindings.has("cache"));
285        assert!(bindings.get_kv("cache").unwrap().is_some());
286        assert!(bindings.get_storage("cache").is_err()); // Wrong type
287    }
288
289    #[test]
290    fn test_external_bindings_serialization() {
291        let mut bindings = ExternalBindings::new();
292        bindings.insert(
293            "data",
294            ExternalBinding::Storage(StorageBinding::s3("test-bucket")),
295        );
296
297        let json = serde_json::to_string(&bindings).unwrap();
298        let deserialized: ExternalBindings = serde_json::from_str(&json).unwrap();
299        assert_eq!(bindings, deserialized);
300    }
301}