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//! - Horizon 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};
9
10/// Represents a container binding for container-to-container or external communication
11#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
12#[serde(tag = "service", rename_all = "lowercase")]
13pub enum ContainerBinding {
14    /// Horizon-managed container binding (AWS/GCP/Azure)
15    Horizon(HorizonContainerBinding),
16    /// Kubernetes container binding
17    Kubernetes(KubernetesContainerBinding),
18    /// Local Docker container binding
19    Local(LocalContainerBinding),
20}
21
22/// Horizon container binding configuration (for cloud platforms)
23#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
24#[serde(rename_all = "camelCase")]
25pub struct HorizonContainerBinding {
26    /// Container name in Horizon
27    pub container_name: BindingValue<String>,
28    /// Internal URL (e.g., "http://api.svc:8080")
29    pub internal_url: BindingValue<String>,
30    /// Optional public URL (if exposed publicly via load balancer)
31    #[serde(skip_serializing_if = "Option::is_none")]
32    pub public_url: Option<BindingValue<String>>,
33}
34
35/// Kubernetes container binding configuration
36#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
37#[serde(rename_all = "camelCase")]
38pub struct KubernetesContainerBinding {
39    /// The container name
40    pub name: BindingValue<String>,
41    /// The Kubernetes namespace
42    pub namespace: BindingValue<String>,
43    /// The Kubernetes Service name
44    pub service_name: BindingValue<String>,
45    /// The Service port
46    pub service_port: BindingValue<u16>,
47    /// Optional public URL if container is exposed publicly
48    #[serde(skip_serializing_if = "Option::is_none")]
49    pub public_url: Option<BindingValue<String>>,
50}
51
52/// Local Docker container binding configuration
53#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
54#[serde(rename_all = "camelCase")]
55pub struct LocalContainerBinding {
56    /// Container name/ID
57    pub container_name: BindingValue<String>,
58    /// Internal URL (Docker network DNS, e.g., "http://api.svc:8080")
59    pub internal_url: BindingValue<String>,
60    /// Optional public URL (localhost with mapped port, e.g., "http://localhost:62844")
61    #[serde(skip_serializing_if = "Option::is_none")]
62    pub public_url: Option<BindingValue<String>>,
63}
64
65impl ContainerBinding {
66    /// Creates a Horizon container binding
67    pub fn horizon(
68        container_name: impl Into<BindingValue<String>>,
69        internal_url: impl Into<BindingValue<String>>,
70    ) -> Self {
71        Self::Horizon(HorizonContainerBinding {
72            container_name: container_name.into(),
73            internal_url: internal_url.into(),
74            public_url: None,
75        })
76    }
77
78    /// Creates a Horizon container binding with public URL
79    pub fn horizon_with_public_url(
80        container_name: impl Into<BindingValue<String>>,
81        internal_url: impl Into<BindingValue<String>>,
82        public_url: impl Into<BindingValue<String>>,
83    ) -> Self {
84        Self::Horizon(HorizonContainerBinding {
85            container_name: container_name.into(),
86            internal_url: internal_url.into(),
87            public_url: Some(public_url.into()),
88        })
89    }
90
91    /// Creates a local Docker container binding
92    pub fn local(
93        container_name: impl Into<BindingValue<String>>,
94        internal_url: impl Into<BindingValue<String>>,
95    ) -> Self {
96        Self::Local(LocalContainerBinding {
97            container_name: container_name.into(),
98            internal_url: internal_url.into(),
99            public_url: None,
100        })
101    }
102
103    /// Creates a local Docker container binding with public URL
104    pub fn local_with_public_url(
105        container_name: impl Into<BindingValue<String>>,
106        internal_url: impl Into<BindingValue<String>>,
107        public_url: impl Into<BindingValue<String>>,
108    ) -> Self {
109        Self::Local(LocalContainerBinding {
110            container_name: container_name.into(),
111            internal_url: internal_url.into(),
112            public_url: Some(public_url.into()),
113        })
114    }
115
116    /// Creates a Kubernetes container binding
117    pub fn kubernetes(
118        name: impl Into<BindingValue<String>>,
119        namespace: impl Into<BindingValue<String>>,
120        service_name: impl Into<BindingValue<String>>,
121        service_port: impl Into<BindingValue<u16>>,
122    ) -> Self {
123        Self::Kubernetes(KubernetesContainerBinding {
124            name: name.into(),
125            namespace: namespace.into(),
126            service_name: service_name.into(),
127            service_port: service_port.into(),
128            public_url: None,
129        })
130    }
131
132    /// Creates a Kubernetes container binding with public URL
133    pub fn kubernetes_with_public_url(
134        name: impl Into<BindingValue<String>>,
135        namespace: impl Into<BindingValue<String>>,
136        service_name: impl Into<BindingValue<String>>,
137        service_port: impl Into<BindingValue<u16>>,
138        public_url: impl Into<BindingValue<String>>,
139    ) -> Self {
140        Self::Kubernetes(KubernetesContainerBinding {
141            name: name.into(),
142            namespace: namespace.into(),
143            service_name: service_name.into(),
144            service_port: service_port.into(),
145            public_url: Some(public_url.into()),
146        })
147    }
148
149    /// Gets the internal URL for any platform
150    /// For Kubernetes, constructs the cluster-local DNS name
151    pub fn get_internal_url(&self) -> Option<String> {
152        match self {
153            ContainerBinding::Horizon(binding) => {
154                // Extract value if it's a concrete value, not a template expression
155                if let BindingValue::Value(url) = &binding.internal_url {
156                    Some(url.clone())
157                } else {
158                    None
159                }
160            }
161            ContainerBinding::Kubernetes(binding) => {
162                // Construct cluster-local DNS name from components
163                if let (
164                    BindingValue::Value(service_name),
165                    BindingValue::Value(namespace),
166                    BindingValue::Value(port),
167                ) = (
168                    &binding.service_name,
169                    &binding.namespace,
170                    &binding.service_port,
171                ) {
172                    Some(format!(
173                        "http://{}.{}.svc.cluster.local:{}",
174                        service_name, namespace, port
175                    ))
176                } else {
177                    None
178                }
179            }
180            ContainerBinding::Local(binding) => {
181                if let BindingValue::Value(url) = &binding.internal_url {
182                    Some(url.clone())
183                } else {
184                    None
185                }
186            }
187        }
188    }
189
190    /// Gets the public URL if available for any platform
191    pub fn get_public_url(&self) -> Option<&BindingValue<String>> {
192        match self {
193            ContainerBinding::Horizon(binding) => binding.public_url.as_ref(),
194            ContainerBinding::Kubernetes(binding) => binding.public_url.as_ref(),
195            ContainerBinding::Local(binding) => binding.public_url.as_ref(),
196        }
197    }
198}
199
200#[cfg(test)]
201mod tests {
202    use super::*;
203
204    #[test]
205    fn test_horizon_binding() {
206        let binding = ContainerBinding::horizon("api", "http://api.svc:8080");
207
208        if let ContainerBinding::Horizon(horizon_binding) = binding {
209            assert_eq!(
210                horizon_binding.container_name,
211                BindingValue::Value("api".to_string())
212            );
213            assert_eq!(
214                horizon_binding.internal_url,
215                BindingValue::Value("http://api.svc:8080".to_string())
216            );
217            assert!(horizon_binding.public_url.is_none());
218        } else {
219            panic!("Expected Horizon binding");
220        }
221    }
222
223    #[test]
224    fn test_horizon_binding_with_public_url() {
225        let binding = ContainerBinding::horizon_with_public_url(
226            "api",
227            "http://api.svc:8080",
228            "https://api.example.com",
229        );
230
231        if let ContainerBinding::Horizon(horizon_binding) = binding {
232            assert_eq!(
233                horizon_binding.public_url,
234                Some(BindingValue::Value("https://api.example.com".to_string()))
235            );
236        } else {
237            panic!("Expected Horizon binding");
238        }
239    }
240
241    #[test]
242    fn test_local_binding() {
243        let binding = ContainerBinding::local("my-container", "http://my-container.svc:8080");
244
245        if let ContainerBinding::Local(local_binding) = binding {
246            assert_eq!(
247                local_binding.container_name,
248                BindingValue::Value("my-container".to_string())
249            );
250            assert_eq!(
251                local_binding.internal_url,
252                BindingValue::Value("http://my-container.svc:8080".to_string())
253            );
254            assert!(local_binding.public_url.is_none());
255        } else {
256            panic!("Expected Local binding");
257        }
258    }
259
260    #[test]
261    fn test_local_binding_with_public_url() {
262        let binding = ContainerBinding::local_with_public_url(
263            "my-container",
264            "http://my-container.svc:8080",
265            "http://localhost:62844",
266        );
267
268        if let ContainerBinding::Local(local_binding) = binding {
269            assert_eq!(
270                local_binding.public_url,
271                Some(BindingValue::Value("http://localhost:62844".to_string()))
272            );
273        } else {
274            panic!("Expected Local binding");
275        }
276    }
277
278    #[test]
279    fn test_get_internal_url() {
280        let horizon = ContainerBinding::horizon("api", "http://api.svc:8080");
281        assert_eq!(
282            horizon.get_internal_url(),
283            Some("http://api.svc:8080".to_string())
284        );
285
286        let local = ContainerBinding::local("api", "http://api.svc:3000");
287        assert_eq!(
288            local.get_internal_url(),
289            Some("http://api.svc:3000".to_string())
290        );
291    }
292
293    #[test]
294    fn test_serialization_roundtrip() {
295        let binding = ContainerBinding::horizon_with_public_url(
296            "api",
297            "http://api.svc:8080",
298            "https://api.example.com",
299        );
300
301        let serialized = serde_json::to_string(&binding).unwrap();
302        let deserialized: ContainerBinding = serde_json::from_str(&serialized).unwrap();
303        assert_eq!(binding, deserialized);
304    }
305}