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