Skip to main content

alien_core/bindings/
container.rs

1//! Container binding definitions for container-to-container communication
2//!
3//! This module defines the binding parameters for container resources:
4//! - Managed cloud containers (AWS/GCP/Azure - using internal DNS and optional public URL)
5//! - Local containers (Docker - using localhost URL)
6
7use super::BindingValue;
8use serde::{Deserialize, Serialize};
9use std::collections::HashMap;
10
11/// Represents a container binding for container-to-container or external communication
12#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
13#[serde(tag = "service", rename_all = "lowercase")]
14pub enum ContainerBinding {
15    /// Managed cloud container binding (AWS/GCP/Azure)
16    Horizon(HorizonContainerBinding),
17    /// Kubernetes container binding
18    Kubernetes(KubernetesContainerBinding),
19    /// Local Docker container binding
20    Local(LocalContainerBinding),
21}
22
23/// Managed cloud container binding configuration (for cloud platforms)
24#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
25#[serde(rename_all = "camelCase")]
26pub struct HorizonContainerBinding {
27    /// Container name in the managed container backend
28    pub container_name: BindingValue<String>,
29    /// Internal URL (e.g., "http://api.svc:8080")
30    pub internal_url: BindingValue<String>,
31    /// Optional public URL (if exposed publicly via load balancer)
32    #[serde(skip_serializing_if = "Option::is_none")]
33    pub public_url: Option<BindingValue<String>>,
34    /// Internal URLs keyed by port, available to every linking resource.
35    #[serde(
36        default,
37        skip_serializing_if = "HashMap::is_empty",
38        deserialize_with = "deserialize_internal_urls"
39    )]
40    pub internal_urls: HashMap<u16, BindingValue<String>>,
41}
42
43/// Kubernetes container binding configuration
44#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
45#[serde(rename_all = "camelCase")]
46pub struct KubernetesContainerBinding {
47    /// The container name
48    pub name: BindingValue<String>,
49    /// The Kubernetes namespace
50    pub namespace: BindingValue<String>,
51    /// The Kubernetes Service name
52    pub service_name: BindingValue<String>,
53    /// The Service port
54    pub service_port: BindingValue<u16>,
55    /// Optional public URL if container is exposed publicly
56    #[serde(skip_serializing_if = "Option::is_none")]
57    pub public_url: Option<BindingValue<String>>,
58    /// Internal URLs keyed by port, available to every linking resource.
59    #[serde(
60        default,
61        skip_serializing_if = "HashMap::is_empty",
62        deserialize_with = "deserialize_internal_urls"
63    )]
64    pub internal_urls: HashMap<u16, BindingValue<String>>,
65}
66
67/// Local Docker container binding configuration
68#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
69#[serde(rename_all = "camelCase")]
70pub struct LocalContainerBinding {
71    /// Container name/ID
72    pub container_name: BindingValue<String>,
73    /// Internal URL (Docker network DNS, e.g., "http://api.svc:8080")
74    pub internal_url: BindingValue<String>,
75    /// Optional public URL (localhost with mapped port, e.g., "http://localhost:62844")
76    #[serde(skip_serializing_if = "Option::is_none")]
77    pub public_url: Option<BindingValue<String>>,
78    /// Internal URLs keyed by port, available to every linking resource.
79    #[serde(
80        default,
81        skip_serializing_if = "HashMap::is_empty",
82        deserialize_with = "deserialize_internal_urls"
83    )]
84    pub internal_urls: HashMap<u16, BindingValue<String>>,
85}
86
87// Internally tagged bindings buffer map keys as strings; parse ports explicitly.
88fn deserialize_internal_urls<'de, D>(
89    deserializer: D,
90) -> Result<HashMap<u16, BindingValue<String>>, D::Error>
91where
92    D: serde::Deserializer<'de>,
93{
94    HashMap::<String, BindingValue<String>>::deserialize(deserializer)?
95        .into_iter()
96        .map(|(port, url)| {
97            port.parse::<u16>()
98                .map(|port| (port, url))
99                .map_err(serde::de::Error::custom)
100        })
101        .collect()
102}
103
104impl ContainerBinding {
105    /// Creates a managed cloud container binding
106    pub fn horizon(
107        container_name: impl Into<BindingValue<String>>,
108        internal_url: impl Into<BindingValue<String>>,
109    ) -> Self {
110        Self::Horizon(HorizonContainerBinding {
111            container_name: container_name.into(),
112            internal_url: internal_url.into(),
113            public_url: None,
114            internal_urls: HashMap::new(),
115        })
116    }
117
118    /// Creates a managed cloud container binding with public URL
119    pub fn horizon_with_public_url(
120        container_name: impl Into<BindingValue<String>>,
121        internal_url: impl Into<BindingValue<String>>,
122        public_url: impl Into<BindingValue<String>>,
123    ) -> Self {
124        Self::Horizon(HorizonContainerBinding {
125            container_name: container_name.into(),
126            internal_url: internal_url.into(),
127            public_url: Some(public_url.into()),
128            internal_urls: HashMap::new(),
129        })
130    }
131
132    /// Creates a local Docker container binding
133    pub fn local(
134        container_name: impl Into<BindingValue<String>>,
135        internal_url: impl Into<BindingValue<String>>,
136    ) -> Self {
137        Self::Local(LocalContainerBinding {
138            container_name: container_name.into(),
139            internal_url: internal_url.into(),
140            public_url: None,
141            internal_urls: HashMap::new(),
142        })
143    }
144
145    /// Creates a local Docker container binding with public URL
146    pub fn local_with_public_url(
147        container_name: impl Into<BindingValue<String>>,
148        internal_url: impl Into<BindingValue<String>>,
149        public_url: impl Into<BindingValue<String>>,
150    ) -> Self {
151        Self::Local(LocalContainerBinding {
152            container_name: container_name.into(),
153            internal_url: internal_url.into(),
154            public_url: Some(public_url.into()),
155            internal_urls: HashMap::new(),
156        })
157    }
158
159    /// Creates a Kubernetes container binding
160    pub fn kubernetes(
161        name: impl Into<BindingValue<String>>,
162        namespace: impl Into<BindingValue<String>>,
163        service_name: impl Into<BindingValue<String>>,
164        service_port: impl Into<BindingValue<u16>>,
165    ) -> Self {
166        Self::Kubernetes(KubernetesContainerBinding {
167            name: name.into(),
168            namespace: namespace.into(),
169            service_name: service_name.into(),
170            service_port: service_port.into(),
171            public_url: None,
172            internal_urls: HashMap::new(),
173        })
174    }
175
176    /// Creates a Kubernetes container binding with public URL
177    pub fn kubernetes_with_public_url(
178        name: impl Into<BindingValue<String>>,
179        namespace: impl Into<BindingValue<String>>,
180        service_name: impl Into<BindingValue<String>>,
181        service_port: impl Into<BindingValue<u16>>,
182        public_url: impl Into<BindingValue<String>>,
183    ) -> Self {
184        Self::Kubernetes(KubernetesContainerBinding {
185            name: name.into(),
186            namespace: namespace.into(),
187            service_name: service_name.into(),
188            service_port: service_port.into(),
189            public_url: Some(public_url.into()),
190            internal_urls: HashMap::new(),
191        })
192    }
193
194    /// Internal URLs keyed by declared port.
195    pub fn internal_urls(&self) -> &HashMap<u16, BindingValue<String>> {
196        match self {
197            Self::Horizon(binding) => &binding.internal_urls,
198            Self::Kubernetes(binding) => &binding.internal_urls,
199            Self::Local(binding) => &binding.internal_urls,
200        }
201    }
202
203    /// Sets the internal URLs supplied by the backend.
204    pub fn with_internal_urls(mut self, endpoints: HashMap<u16, BindingValue<String>>) -> Self {
205        match &mut self {
206            Self::Horizon(binding) => binding.internal_urls = endpoints,
207            Self::Kubernetes(binding) => binding.internal_urls = endpoints,
208            Self::Local(binding) => binding.internal_urls = endpoints,
209        }
210        self
211    }
212
213    /// Gets the internal URL for any platform
214    /// For Kubernetes, constructs the cluster-local DNS name
215    pub fn get_internal_url(&self) -> Option<String> {
216        match self {
217            ContainerBinding::Horizon(binding) => {
218                // Extract value if it's a concrete value, not a template expression
219                if let BindingValue::Value(url) = &binding.internal_url {
220                    Some(url.clone())
221                } else {
222                    None
223                }
224            }
225            ContainerBinding::Kubernetes(binding) => {
226                // Construct cluster-local DNS name from components
227                if let (
228                    BindingValue::Value(service_name),
229                    BindingValue::Value(namespace),
230                    BindingValue::Value(port),
231                ) = (
232                    &binding.service_name,
233                    &binding.namespace,
234                    &binding.service_port,
235                ) {
236                    Some(format!(
237                        "http://{}.{}.svc.cluster.local:{}",
238                        service_name, namespace, port
239                    ))
240                } else {
241                    None
242                }
243            }
244            ContainerBinding::Local(binding) => {
245                if let BindingValue::Value(url) = &binding.internal_url {
246                    Some(url.clone())
247                } else {
248                    None
249                }
250            }
251        }
252    }
253
254    /// Gets the public URL if available for any platform
255    pub fn get_public_url(&self) -> Option<&BindingValue<String>> {
256        match self {
257            ContainerBinding::Horizon(binding) => binding.public_url.as_ref(),
258            ContainerBinding::Kubernetes(binding) => binding.public_url.as_ref(),
259            ContainerBinding::Local(binding) => binding.public_url.as_ref(),
260        }
261    }
262}
263
264#[cfg(test)]
265mod tests {
266    use super::*;
267
268    #[test]
269    fn test_horizon_binding() {
270        let binding = ContainerBinding::horizon("api", "http://api.svc:8080");
271
272        if let ContainerBinding::Horizon(horizon_binding) = binding {
273            assert_eq!(
274                horizon_binding.container_name,
275                BindingValue::Value("api".to_string())
276            );
277            assert_eq!(
278                horizon_binding.internal_url,
279                BindingValue::Value("http://api.svc:8080".to_string())
280            );
281            assert!(horizon_binding.public_url.is_none());
282        } else {
283            panic!("Expected Horizon binding");
284        }
285    }
286
287    #[test]
288    fn test_horizon_binding_with_public_url() {
289        let binding = ContainerBinding::horizon_with_public_url(
290            "api",
291            "http://api.svc:8080",
292            "https://api.example.com",
293        );
294
295        if let ContainerBinding::Horizon(horizon_binding) = binding {
296            assert_eq!(
297                horizon_binding.public_url,
298                Some(BindingValue::Value("https://api.example.com".to_string()))
299            );
300        } else {
301            panic!("Expected Horizon binding");
302        }
303    }
304
305    #[test]
306    fn test_local_binding() {
307        let binding = ContainerBinding::local("my-container", "http://my-container.svc:8080");
308
309        if let ContainerBinding::Local(local_binding) = binding {
310            assert_eq!(
311                local_binding.container_name,
312                BindingValue::Value("my-container".to_string())
313            );
314            assert_eq!(
315                local_binding.internal_url,
316                BindingValue::Value("http://my-container.svc:8080".to_string())
317            );
318            assert!(local_binding.public_url.is_none());
319        } else {
320            panic!("Expected Local binding");
321        }
322    }
323
324    #[test]
325    fn test_local_binding_with_public_url() {
326        let binding = ContainerBinding::local_with_public_url(
327            "my-container",
328            "http://my-container.svc:8080",
329            "http://localhost:62844",
330        );
331
332        if let ContainerBinding::Local(local_binding) = binding {
333            assert_eq!(
334                local_binding.public_url,
335                Some(BindingValue::Value("http://localhost:62844".to_string()))
336            );
337        } else {
338            panic!("Expected Local binding");
339        }
340    }
341
342    #[test]
343    fn test_get_internal_url() {
344        let horizon = ContainerBinding::horizon("api", "http://api.svc:8080");
345        assert_eq!(
346            horizon.get_internal_url(),
347            Some("http://api.svc:8080".to_string())
348        );
349
350        let local = ContainerBinding::local("api", "http://api.svc:3000");
351        assert_eq!(
352            local.get_internal_url(),
353            Some("http://api.svc:3000".to_string())
354        );
355    }
356
357    #[test]
358    fn test_serialization_roundtrip() {
359        let binding = ContainerBinding::horizon_with_public_url(
360            "api",
361            "http://api.svc:8080",
362            "https://api.example.com",
363        );
364
365        let serialized = serde_json::to_string(&binding).unwrap();
366        let deserialized: ContainerBinding = serde_json::from_str(&serialized).unwrap();
367        assert_eq!(binding, deserialized);
368    }
369}