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, ExternalAiBinding,
14    KvBinding, PostgresBinding, QueueBinding, RedisBinding, 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    /// External Redis binding (operator-provided Redis-compatible server)
44    Redis(RedisBinding),
45    /// External AI provider binding (BYO-key OpenAI/Anthropic, etc.)
46    Ai(ExternalAiBinding),
47}
48
49/// Map from resource ID to external binding.
50///
51/// Validated at runtime: binding type must match resource type.
52#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)]
53#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
54#[serde(transparent)]
55pub struct ExternalBindings(pub HashMap<String, ExternalBinding>);
56
57impl ExternalBindings {
58    /// Creates an empty ExternalBindings map.
59    pub fn new() -> Self {
60        Self(HashMap::new())
61    }
62
63    /// Returns true if there are no external bindings.
64    pub fn is_empty(&self) -> bool {
65        self.0.is_empty()
66    }
67
68    /// Checks if a binding exists for the given resource ID.
69    pub fn has(&self, resource_id: &str) -> bool {
70        self.0.contains_key(resource_id)
71    }
72
73    /// Gets an external binding by resource ID.
74    pub fn get(&self, resource_id: &str) -> Option<&ExternalBinding> {
75        self.0.get(resource_id)
76    }
77
78    /// Gets a storage binding for the given resource ID.
79    /// Returns an error if the binding exists but is not a Storage type.
80    pub fn get_storage(&self, id: &str) -> crate::error::Result<Option<&StorageBinding>> {
81        match self.0.get(id) {
82            Some(ExternalBinding::Storage(b)) => Ok(Some(b)),
83            Some(other) => Err(AlienError::new(ErrorData::ExternalBindingTypeMismatch {
84                resource_id: id.to_string(),
85                expected: "storage".to_string(),
86                actual: other.binding_type().to_string(),
87            })),
88            None => Ok(None),
89        }
90    }
91
92    /// Gets a queue binding for the given resource ID.
93    /// Returns an error if the binding exists but is not a Queue type.
94    pub fn get_queue(&self, id: &str) -> crate::error::Result<Option<&QueueBinding>> {
95        match self.0.get(id) {
96            Some(ExternalBinding::Queue(b)) => Ok(Some(b)),
97            Some(other) => Err(AlienError::new(ErrorData::ExternalBindingTypeMismatch {
98                resource_id: id.to_string(),
99                expected: "queue".to_string(),
100                actual: other.binding_type().to_string(),
101            })),
102            None => Ok(None),
103        }
104    }
105
106    /// Gets a KV binding for the given resource ID.
107    /// Returns an error if the binding exists but is not a Kv type.
108    pub fn get_kv(&self, id: &str) -> crate::error::Result<Option<&KvBinding>> {
109        match self.0.get(id) {
110            Some(ExternalBinding::Kv(b)) => Ok(Some(b)),
111            Some(other) => Err(AlienError::new(ErrorData::ExternalBindingTypeMismatch {
112                resource_id: id.to_string(),
113                expected: "kv".to_string(),
114                actual: other.binding_type().to_string(),
115            })),
116            None => Ok(None),
117        }
118    }
119
120    /// Gets an artifact registry binding for the given resource ID.
121    /// Returns an error if the binding exists but is not an ArtifactRegistry type.
122    pub fn get_artifact_registry(
123        &self,
124        id: &str,
125    ) -> crate::error::Result<Option<&ArtifactRegistryBinding>> {
126        match self.0.get(id) {
127            Some(ExternalBinding::ArtifactRegistry(b)) => Ok(Some(b)),
128            Some(other) => Err(AlienError::new(ErrorData::ExternalBindingTypeMismatch {
129                resource_id: id.to_string(),
130                expected: "artifact_registry".to_string(),
131                actual: other.binding_type().to_string(),
132            })),
133            None => Ok(None),
134        }
135    }
136
137    /// Gets a vault binding for the given resource ID.
138    /// Returns an error if the binding exists but is not a Vault type.
139    pub fn get_vault(&self, id: &str) -> crate::error::Result<Option<&VaultBinding>> {
140        match self.0.get(id) {
141            Some(ExternalBinding::Vault(b)) => Ok(Some(b)),
142            Some(other) => Err(AlienError::new(ErrorData::ExternalBindingTypeMismatch {
143                resource_id: id.to_string(),
144                expected: "vault".to_string(),
145                actual: other.binding_type().to_string(),
146            })),
147            None => Ok(None),
148        }
149    }
150
151    /// Gets an AI binding for the given resource ID.
152    /// Returns an error if the binding exists but is not an Ai type.
153    pub fn get_ai(&self, id: &str) -> crate::error::Result<Option<&ExternalAiBinding>> {
154        match self.0.get(id) {
155            Some(ExternalBinding::Ai(b)) => Ok(Some(b)),
156            Some(other) => Err(AlienError::new(ErrorData::ExternalBindingTypeMismatch {
157                resource_id: id.to_string(),
158                expected: "ai".to_string(),
159                actual: other.binding_type().to_string(),
160            })),
161            None => Ok(None),
162        }
163    }
164
165    /// Gets a container apps environment binding for the given resource ID.
166    /// Returns an error if the binding exists but is not a ContainerAppsEnvironment type.
167    pub fn get_container_apps_environment(
168        &self,
169        id: &str,
170    ) -> crate::error::Result<Option<&ContainerAppsEnvironmentBinding>> {
171        match self.0.get(id) {
172            Some(ExternalBinding::ContainerAppsEnvironment(b)) => Ok(Some(b)),
173            Some(other) => Err(AlienError::new(ErrorData::ExternalBindingTypeMismatch {
174                resource_id: id.to_string(),
175                expected: "azure_container_apps_environment".to_string(),
176                actual: other.binding_type().to_string(),
177            })),
178            None => Ok(None),
179        }
180    }
181
182    /// Returns an error if the binding exists but is not a Postgres type.
183    pub fn get_postgres(&self, id: &str) -> crate::error::Result<Option<&PostgresBinding>> {
184        match self.0.get(id) {
185            Some(ExternalBinding::Postgres(b)) => Ok(Some(b)),
186            Some(other) => Err(AlienError::new(ErrorData::ExternalBindingTypeMismatch {
187                resource_id: id.to_string(),
188                expected: "postgres".to_string(),
189                actual: other.binding_type().to_string(),
190            })),
191            None => Ok(None),
192        }
193    }
194
195    /// Returns an error if the binding exists but is not a Redis type.
196    pub fn get_redis(&self, id: &str) -> crate::error::Result<Option<&RedisBinding>> {
197        match self.0.get(id) {
198            Some(ExternalBinding::Redis(b)) => Ok(Some(b)),
199            Some(other) => Err(AlienError::new(ErrorData::ExternalBindingTypeMismatch {
200                resource_id: id.to_string(),
201                expected: "redis".to_string(),
202                actual: other.binding_type().to_string(),
203            })),
204            None => Ok(None),
205        }
206    }
207
208    /// Inserts an external binding for a resource.
209    pub fn insert(&mut self, resource_id: impl Into<String>, binding: ExternalBinding) {
210        self.0.insert(resource_id.into(), binding);
211    }
212}
213
214impl ExternalBinding {
215    /// Returns the type name of this binding variant.
216    pub fn binding_type(&self) -> &'static str {
217        match self {
218            ExternalBinding::Storage(_) => "storage",
219            ExternalBinding::Queue(_) => "queue",
220            ExternalBinding::Kv(_) => "kv",
221            ExternalBinding::ArtifactRegistry(_) => "artifact_registry",
222            ExternalBinding::Vault(_) => "vault",
223            ExternalBinding::ContainerAppsEnvironment(_) => "azure_container_apps_environment",
224            ExternalBinding::Postgres(_) => "postgres",
225            ExternalBinding::Redis(_) => "redis",
226            ExternalBinding::Ai(_) => "ai",
227        }
228    }
229
230    /// Converts this external binding into resource outputs that dependent resources
231    /// can read via `get_resource_outputs()`.
232    ///
233    /// Infrastructure bindings (Container Apps Environment) produce typed outputs so that
234    /// dependent resources like functions and builds can read the environment's name,
235    /// resource ID, and resource group. Application-level bindings (Storage, Queue, KV, etc.)
236    /// return `None` — they are consumed via `remote_binding_params` and environment variables
237    /// rather than `get_resource_outputs()`.
238    pub fn to_resource_outputs(&self) -> Option<ResourceOutputs> {
239        match self {
240            ExternalBinding::ContainerAppsEnvironment(binding) => {
241                // Extract concrete values from BindingValue wrappers.
242                // External bindings for pre-provisioned resources always use concrete values.
243                let environment_name = match &binding.environment_name {
244                    BindingValue::Value(v) => v.clone(),
245                    _ => return None,
246                };
247                let resource_id = match &binding.resource_id {
248                    BindingValue::Value(v) => v.clone(),
249                    _ => return None,
250                };
251                let resource_group_name = match &binding.resource_group_name {
252                    BindingValue::Value(v) => v.clone(),
253                    _ => return None,
254                };
255                let default_domain = match &binding.default_domain {
256                    BindingValue::Value(v) => v.clone(),
257                    _ => return None,
258                };
259                let static_ip = binding.static_ip.as_ref().and_then(|v| match v {
260                    BindingValue::Value(v) => Some(v.clone()),
261                    _ => None,
262                });
263
264                Some(ResourceOutputs::new(AzureContainerAppsEnvironmentOutputs {
265                    environment_name,
266                    resource_id,
267                    resource_group_name,
268                    default_domain,
269                    static_ip,
270                    custom_domain_verification_id: None,
271                }))
272            }
273            // Application-level bindings are consumed via remote_binding_params, not outputs
274            _ => None,
275        }
276    }
277
278    /// Serializes this external binding to the JSON value injected into the worker
279    /// environment as `ALIEN_<NAME>_BINDING`, matching the shape the SDK parses.
280    ///
281    /// The AI arm wraps the carried `ExternalAiBinding` in `AiBinding::External` so
282    /// the value keeps the `service` tag the SDK's `ai(name)` parser discriminates
283    /// on; a bare `ExternalAiBinding` would lack it. Every other arm serializes the
284    /// carried binding directly, like the runtime controllers' `get_binding_params`.
285    pub fn to_env_binding_value(&self) -> serde_json::Result<serde_json::Value> {
286        match self {
287            ExternalBinding::Storage(b) => serde_json::to_value(b),
288            ExternalBinding::Queue(b) => serde_json::to_value(b),
289            ExternalBinding::Kv(b) => serde_json::to_value(b),
290            ExternalBinding::ArtifactRegistry(b) => serde_json::to_value(b),
291            ExternalBinding::Vault(b) => serde_json::to_value(b),
292            ExternalBinding::ContainerAppsEnvironment(b) => serde_json::to_value(b),
293            ExternalBinding::Postgres(b) => serde_json::to_value(b),
294            ExternalBinding::Redis(b) => serde_json::to_value(b),
295            ExternalBinding::Ai(b) => {
296                serde_json::to_value(crate::bindings::AiBinding::External(b.clone()))
297            }
298        }
299    }
300}
301
302/// Validates that an external binding type matches the resource type.
303pub fn validate_binding_type(
304    resource: &Resource,
305    binding: &ExternalBinding,
306) -> crate::error::Result<()> {
307    let resource_type = resource.resource_type();
308    let resource_type_str = resource_type.as_ref();
309
310    let valid = match (resource_type_str, binding) {
311        ("storage", ExternalBinding::Storage(_)) => true,
312        ("queue", ExternalBinding::Queue(_)) => true,
313        ("kv", ExternalBinding::Kv(_)) => true,
314        ("artifact_registry", ExternalBinding::ArtifactRegistry(_)) => true,
315        ("vault", ExternalBinding::Vault(_)) => true,
316        ("azure_container_apps_environment", ExternalBinding::ContainerAppsEnvironment(_)) => true,
317        ("postgres", ExternalBinding::Postgres(_)) => true,
318        ("redis", ExternalBinding::Redis(_)) => true,
319        ("ai", ExternalBinding::Ai(_)) => true,
320        _ => false,
321    };
322
323    if !valid {
324        return Err(AlienError::new(ErrorData::ExternalBindingTypeMismatch {
325            resource_id: resource.id().to_string(),
326            expected: resource_type_str.to_string(),
327            actual: binding.binding_type().to_string(),
328        }));
329    }
330    Ok(())
331}
332
333#[cfg(test)]
334mod tests {
335    use super::*;
336    use crate::bindings::{KvBinding, StorageBinding};
337
338    #[test]
339    fn test_external_bindings_storage() {
340        let mut bindings = ExternalBindings::new();
341        bindings.insert(
342            "data-storage",
343            ExternalBinding::Storage(StorageBinding::s3("my-bucket")),
344        );
345
346        assert!(bindings.has("data-storage"));
347        assert!(bindings.get_storage("data-storage").unwrap().is_some());
348        assert!(bindings.get_queue("data-storage").is_err()); // Wrong type
349    }
350
351    #[test]
352    fn test_external_bindings_kv() {
353        let mut bindings = ExternalBindings::new();
354        bindings.insert(
355            "cache",
356            ExternalBinding::Kv(KvBinding::redis("redis://localhost:6379")),
357        );
358
359        assert!(bindings.has("cache"));
360        assert!(bindings.get_kv("cache").unwrap().is_some());
361        assert!(bindings.get_storage("cache").is_err()); // Wrong type
362    }
363
364    #[test]
365    fn test_external_bindings_ai() {
366        use crate::bindings::ExternalAiBinding;
367
368        let mut bindings = ExternalBindings::new();
369        bindings.insert(
370            "llm",
371            ExternalBinding::Ai(ExternalAiBinding {
372                provider: "openai".to_string(),
373                api_key: "sk-test".into(),
374            }),
375        );
376
377        assert!(bindings.has("llm"));
378        assert_eq!(bindings.get("llm").unwrap().binding_type(), "ai");
379        assert_eq!(bindings.get_ai("llm").unwrap().unwrap().provider, "openai");
380        assert!(bindings.get_kv("llm").is_err()); // Wrong type
381    }
382
383    #[test]
384    fn test_external_bindings_serialization() {
385        let mut bindings = ExternalBindings::new();
386        bindings.insert(
387            "data",
388            ExternalBinding::Storage(StorageBinding::s3("test-bucket")),
389        );
390
391        let json = serde_json::to_string(&bindings).unwrap();
392        let deserialized: ExternalBindings = serde_json::from_str(&json).unwrap();
393        assert_eq!(bindings, deserialized);
394    }
395}