Skip to main content

alien_core/resources/
network.rs

1//! Network resource for cloud-agnostic VPC/VNet infrastructure.
2//!
3//! The Network resource provides cloud-agnostic networking infrastructure that can be
4//! shared across multiple resource types. Users configure network settings in StackSettings,
5//! and the NetworkMutation preflight auto-generates the Network resource.
6
7use crate::error::{ErrorData, Result};
8use crate::resource::{ResourceDefinition, ResourceOutputsDefinition, ResourceRef};
9use crate::stack_settings::NetworkSettings;
10use crate::ResourceType;
11use alien_error::AlienError;
12use bon::Builder;
13use serde::{Deserialize, Serialize};
14use std::any::Any;
15use std::fmt::Debug;
16
17/// Represents cloud-agnostic networking infrastructure (VPC, VNet, subnets, etc.).
18///
19/// This resource is **always auto-generated** by the NetworkMutation preflight based on
20/// `StackSettings.network`. Users don't define Network resources directly in `alien.ts`.
21#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Builder)]
22#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
23#[serde(rename_all = "camelCase", deny_unknown_fields)]
24#[builder(start_fn = new)]
25pub struct Network {
26    /// Unique identifier for the network resource (typically "default-network")
27    #[builder(start_fn)]
28    pub id: String,
29
30    /// Network configuration (copied from StackSettings)
31    pub settings: NetworkSettings,
32}
33
34impl Network {
35    /// The resource type identifier for Network
36    pub const RESOURCE_TYPE: ResourceType = ResourceType::from_static("network");
37
38    /// Returns the network's unique identifier.
39    pub fn id(&self) -> &str {
40        &self.id
41    }
42}
43
44/// Outputs generated by a successfully provisioned Network.
45///
46/// Network outputs are **cloud-agnostic** and primarily for observability/debugging.
47/// Resources that need platform-specific details (VPC ID, subnet IDs) use `require_dependency`
48/// to access the controller state directly.
49#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
50#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
51#[serde(rename_all = "camelCase")]
52pub struct NetworkOutputs {
53    /// Human-readable network identifier (VPC ID, VNet name, etc.)
54    pub network_id: String,
55
56    /// Number of availability zones / regions used
57    pub availability_zones: u8,
58
59    /// Whether public subnets exist
60    pub has_public_subnets: bool,
61
62    /// Whether NAT gateway exists
63    pub has_nat_gateway: bool,
64
65    /// CIDR block (if created by Alien, None for BYO-VPC)
66    pub cidr: Option<String>,
67}
68
69impl ResourceOutputsDefinition for NetworkOutputs {
70    fn get_resource_type(&self) -> ResourceType {
71        Network::RESOURCE_TYPE.clone()
72    }
73
74    fn as_any(&self) -> &dyn Any {
75        self
76    }
77
78    fn box_clone(&self) -> Box<dyn ResourceOutputsDefinition> {
79        Box::new(self.clone())
80    }
81
82    fn outputs_eq(&self, other: &dyn ResourceOutputsDefinition) -> bool {
83        other.as_any().downcast_ref::<NetworkOutputs>() == Some(self)
84    }
85
86    fn to_json_value(&self) -> serde_json::Result<serde_json::Value> {
87        serde_json::to_value(self)
88    }
89}
90
91impl ResourceDefinition for Network {
92    fn get_resource_type(&self) -> ResourceType {
93        Self::RESOURCE_TYPE
94    }
95
96    fn id(&self) -> &str {
97        &self.id
98    }
99
100    fn get_dependencies(&self) -> Vec<ResourceRef> {
101        Vec::new() // Network has no dependencies
102    }
103
104    fn validate_update(&self, new_config: &dyn ResourceDefinition) -> Result<()> {
105        let new_network = new_config
106            .as_any()
107            .downcast_ref::<Network>()
108            .ok_or_else(|| {
109                AlienError::new(ErrorData::UnexpectedResourceType {
110                    resource_id: self.id.clone(),
111                    expected: Self::RESOURCE_TYPE,
112                    actual: new_config.get_resource_type(),
113                })
114            })?;
115
116        if self.id != new_network.id {
117            return Err(AlienError::new(ErrorData::InvalidResourceUpdate {
118                resource_id: self.id.clone(),
119                reason: "the 'id' field is immutable".to_string(),
120            }));
121        }
122
123        // Validate that network type doesn't change (e.g., from Create to BYO-VPC)
124        // This would require recreating all networking infrastructure
125        match (&self.settings, &new_network.settings) {
126            (NetworkSettings::UseDefault, NetworkSettings::UseDefault) => Ok(()),
127            (NetworkSettings::Create { .. }, NetworkSettings::Create { .. }) => Ok(()),
128            (NetworkSettings::ByoVpcAws { .. }, NetworkSettings::ByoVpcAws { .. }) => Ok(()),
129            (NetworkSettings::ByoVpcGcp { .. }, NetworkSettings::ByoVpcGcp { .. }) => Ok(()),
130            (NetworkSettings::ByoVnetAzure { .. }, NetworkSettings::ByoVnetAzure { .. }) => Ok(()),
131            _ => Err(AlienError::new(ErrorData::InvalidResourceUpdate {
132                resource_id: self.id.clone(),
133                reason: "cannot change network type (e.g., from 'create' to 'use-default')"
134                    .to_string(),
135            })),
136        }
137    }
138
139    fn as_any(&self) -> &dyn Any {
140        self
141    }
142
143    fn as_any_mut(&mut self) -> &mut dyn Any {
144        self
145    }
146
147    fn box_clone(&self) -> Box<dyn ResourceDefinition> {
148        Box::new(self.clone())
149    }
150
151    fn resource_eq(&self, other: &dyn ResourceDefinition) -> bool {
152        other.as_any().downcast_ref::<Network>() == Some(self)
153    }
154
155    fn to_json_value(&self) -> serde_json::Result<serde_json::Value> {
156        serde_json::to_value(self)
157    }
158}
159
160#[cfg(test)]
161mod tests {
162    use super::*;
163
164    #[test]
165    fn test_network_create_settings() {
166        let network = Network::new("default-network".to_string())
167            .settings(NetworkSettings::Create {
168                cidr: Some("10.0.0.0/16".to_string()),
169                availability_zones: 3,
170            })
171            .build();
172
173        assert_eq!(network.id(), "default-network");
174        match &network.settings {
175            NetworkSettings::Create {
176                cidr,
177                availability_zones,
178            } => {
179                assert_eq!(cidr.as_deref(), Some("10.0.0.0/16"));
180                assert_eq!(*availability_zones, 3);
181            }
182            _ => panic!("Expected Create settings"),
183        }
184    }
185
186    #[test]
187    fn test_network_byo_vpc_aws_settings() {
188        let network = Network::new("default-network".to_string())
189            .settings(NetworkSettings::ByoVpcAws {
190                vpc_id: "vpc-12345".to_string(),
191                public_subnet_ids: vec!["subnet-pub-1".to_string(), "subnet-pub-2".to_string()],
192                private_subnet_ids: vec!["subnet-priv-1".to_string(), "subnet-priv-2".to_string()],
193                security_group_ids: vec!["sg-123".to_string()],
194            })
195            .build();
196
197        match &network.settings {
198            NetworkSettings::ByoVpcAws {
199                vpc_id,
200                public_subnet_ids,
201                private_subnet_ids,
202                security_group_ids,
203            } => {
204                assert_eq!(vpc_id, "vpc-12345");
205                assert_eq!(public_subnet_ids.len(), 2);
206                assert_eq!(private_subnet_ids.len(), 2);
207                assert_eq!(security_group_ids.len(), 1);
208            }
209            _ => panic!("Expected ByoVpcAws settings"),
210        }
211    }
212
213    #[test]
214    fn test_network_validate_update_same_type() {
215        let network1 = Network::new("default-network".to_string())
216            .settings(NetworkSettings::Create {
217                cidr: Some("10.0.0.0/16".to_string()),
218                availability_zones: 2,
219            })
220            .build();
221
222        let network2 = Network::new("default-network".to_string())
223            .settings(NetworkSettings::Create {
224                cidr: Some("10.0.0.0/16".to_string()),
225                availability_zones: 3, // Changed
226            })
227            .build();
228
229        // Same network type (Create) should be allowed
230        assert!(network1.validate_update(&network2).is_ok());
231    }
232
233    #[test]
234    fn test_network_validate_update_different_type_fails() {
235        let network1 = Network::new("default-network".to_string())
236            .settings(NetworkSettings::Create {
237                cidr: Some("10.0.0.0/16".to_string()),
238                availability_zones: 2,
239            })
240            .build();
241
242        let network2 = Network::new("default-network".to_string())
243            .settings(NetworkSettings::ByoVpcAws {
244                vpc_id: "vpc-12345".to_string(),
245                public_subnet_ids: vec![],
246                private_subnet_ids: vec![],
247                security_group_ids: vec![],
248            })
249            .build();
250
251        // Changing network type should fail
252        let result = network1.validate_update(&network2);
253        assert!(result.is_err());
254    }
255
256    #[test]
257    fn test_network_validate_update_use_default() {
258        let network1 = Network::new("default-network".to_string())
259            .settings(NetworkSettings::UseDefault)
260            .build();
261
262        let network2 = Network::new("default-network".to_string())
263            .settings(NetworkSettings::UseDefault)
264            .build();
265
266        assert!(network1.validate_update(&network2).is_ok());
267    }
268
269    #[test]
270    fn test_network_validate_update_use_default_to_create_fails() {
271        let network1 = Network::new("default-network".to_string())
272            .settings(NetworkSettings::UseDefault)
273            .build();
274
275        let network2 = Network::new("default-network".to_string())
276            .settings(NetworkSettings::Create {
277                cidr: None,
278                availability_zones: 2,
279            })
280            .build();
281
282        assert!(network1.validate_update(&network2).is_err());
283    }
284
285    #[test]
286    fn test_network_serialization() {
287        let network = Network::new("default-network".to_string())
288            .settings(NetworkSettings::Create {
289                cidr: None,
290                availability_zones: 2,
291            })
292            .build();
293
294        let json = serde_json::to_string(&network).unwrap();
295        let deserialized: Network = serde_json::from_str(&json).unwrap();
296        assert_eq!(network, deserialized);
297    }
298}