1use crate::error::{ErrorData, Result};
8use crate::resource::{ResourceDefinition, ResourceOutputsDefinition, ResourceRef};
9use crate::stack_settings::NetworkSettings;
10use crate::{ResourceType, Stack};
11use alien_error::AlienError;
12use bon::Builder;
13use serde::{Deserialize, Serialize};
14use std::any::Any;
15use std::fmt::Debug;
16
17#[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 #[builder(start_fn)]
28 pub id: String,
29
30 pub settings: NetworkSettings,
32}
33
34impl Network {
35 pub const RESOURCE_TYPE: ResourceType = ResourceType::from_static("network");
37
38 pub fn id(&self) -> &str {
40 &self.id
41 }
42
43 pub fn preflight_settings(stack: &Stack) -> Option<NetworkSettings> {
50 stack
51 .resources()
52 .find_map(|(_, entry)| entry.config.downcast_ref::<Network>())
53 .filter(|network| matches!(network.settings, NetworkSettings::Create { .. }))
54 .map(|network| network.settings.clone())
55 }
56}
57
58#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
64#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
65#[serde(rename_all = "camelCase")]
66pub struct NetworkOutputs {
67 pub network_id: String,
69
70 pub availability_zones: u8,
72
73 pub has_public_subnets: bool,
75
76 pub has_nat_gateway: bool,
78
79 #[serde(default)]
82 pub egress_public_ips: Vec<String>,
83
84 pub cidr: Option<String>,
86}
87
88impl ResourceOutputsDefinition for NetworkOutputs {
89 fn get_resource_type(&self) -> ResourceType {
90 Network::RESOURCE_TYPE.clone()
91 }
92
93 fn as_any(&self) -> &dyn Any {
94 self
95 }
96
97 fn box_clone(&self) -> Box<dyn ResourceOutputsDefinition> {
98 Box::new(self.clone())
99 }
100
101 fn outputs_eq(&self, other: &dyn ResourceOutputsDefinition) -> bool {
102 other.as_any().downcast_ref::<NetworkOutputs>() == Some(self)
103 }
104
105 fn to_json_value(&self) -> serde_json::Result<serde_json::Value> {
106 serde_json::to_value(self)
107 }
108}
109
110impl ResourceDefinition for Network {
111 fn get_resource_type(&self) -> ResourceType {
112 Self::RESOURCE_TYPE
113 }
114
115 fn id(&self) -> &str {
116 &self.id
117 }
118
119 fn get_dependencies(&self) -> Vec<ResourceRef> {
120 Vec::new() }
122
123 fn validate_update(&self, new_config: &dyn ResourceDefinition) -> Result<()> {
124 let new_network = new_config
125 .as_any()
126 .downcast_ref::<Network>()
127 .ok_or_else(|| {
128 AlienError::new(ErrorData::UnexpectedResourceType {
129 resource_id: self.id.clone(),
130 expected: Self::RESOURCE_TYPE,
131 actual: new_config.get_resource_type(),
132 })
133 })?;
134
135 if self.id != new_network.id {
136 return Err(AlienError::new(ErrorData::InvalidResourceUpdate {
137 resource_id: self.id.clone(),
138 reason: "the 'id' field is immutable".to_string(),
139 }));
140 }
141
142 match (&self.settings, &new_network.settings) {
145 (NetworkSettings::UseDefault, NetworkSettings::UseDefault) => Ok(()),
146 (
147 NetworkSettings::Create {
148 availability_zones: old_zones,
149 ..
150 },
151 NetworkSettings::Create {
152 availability_zones: new_zones,
153 ..
154 },
155 ) => {
156 if old_zones != new_zones {
158 return Err(AlienError::new(ErrorData::InvalidResourceUpdate {
159 resource_id: self.id.clone(),
160 reason: format!(
161 "the network's availability zone count can't change after it is created ({old_zones} to {new_zones}); create a new deployment to use a different count"
162 ),
163 }));
164 }
165 Ok(())
166 }
167 (NetworkSettings::ByoVpcAws { .. }, NetworkSettings::ByoVpcAws { .. }) => Ok(()),
168 (NetworkSettings::ByoVpcGcp { .. }, NetworkSettings::ByoVpcGcp { .. }) => Ok(()),
169 (NetworkSettings::ByoVnetAzure { .. }, NetworkSettings::ByoVnetAzure { .. }) => Ok(()),
170 _ => Err(AlienError::new(ErrorData::InvalidResourceUpdate {
171 resource_id: self.id.clone(),
172 reason: "cannot change network type (e.g., from 'create' to 'use-default')"
173 .to_string(),
174 })),
175 }
176 }
177
178 fn as_any(&self) -> &dyn Any {
179 self
180 }
181
182 fn as_any_mut(&mut self) -> &mut dyn Any {
183 self
184 }
185
186 fn box_clone(&self) -> Box<dyn ResourceDefinition> {
187 Box::new(self.clone())
188 }
189
190 fn resource_eq(&self, other: &dyn ResourceDefinition) -> bool {
191 other.as_any().downcast_ref::<Network>() == Some(self)
192 }
193
194 fn to_json_value(&self) -> serde_json::Result<serde_json::Value> {
195 serde_json::to_value(self)
196 }
197}
198
199#[cfg(test)]
200mod tests {
201 use super::*;
202
203 #[test]
204 fn test_network_create_settings() {
205 let network = Network::new("default-network".to_string())
206 .settings(NetworkSettings::Create {
207 cidr: Some("10.0.0.0/16".to_string()),
208 availability_zones: 3,
209 })
210 .build();
211
212 assert_eq!(network.id(), "default-network");
213 match &network.settings {
214 NetworkSettings::Create {
215 cidr,
216 availability_zones,
217 } => {
218 assert_eq!(cidr.as_deref(), Some("10.0.0.0/16"));
219 assert_eq!(*availability_zones, 3);
220 }
221 _ => panic!("Expected Create settings"),
222 }
223 }
224
225 #[test]
226 fn test_network_byo_vpc_aws_settings() {
227 let network = Network::new("default-network".to_string())
228 .settings(NetworkSettings::ByoVpcAws {
229 vpc_id: "vpc-12345".to_string(),
230 public_subnet_ids: vec!["subnet-pub-1".to_string(), "subnet-pub-2".to_string()],
231 private_subnet_ids: vec!["subnet-priv-1".to_string(), "subnet-priv-2".to_string()],
232 security_group_ids: vec!["sg-123".to_string()],
233 })
234 .build();
235
236 match &network.settings {
237 NetworkSettings::ByoVpcAws {
238 vpc_id,
239 public_subnet_ids,
240 private_subnet_ids,
241 security_group_ids,
242 } => {
243 assert_eq!(vpc_id, "vpc-12345");
244 assert_eq!(public_subnet_ids.len(), 2);
245 assert_eq!(private_subnet_ids.len(), 2);
246 assert_eq!(security_group_ids.len(), 1);
247 }
248 _ => panic!("Expected ByoVpcAws settings"),
249 }
250 }
251
252 #[test]
253 fn test_network_validate_update_same_type() {
254 let network = Network::new("default-network".to_string())
255 .settings(NetworkSettings::Create {
256 cidr: Some("10.0.0.0/16".to_string()),
257 availability_zones: 2,
258 })
259 .build();
260
261 assert!(network.validate_update(&network.clone()).is_ok());
262 }
263
264 #[test]
265 fn test_network_validate_update_availability_zone_change_fails() {
266 let network1 = Network::new("default-network".to_string())
267 .settings(NetworkSettings::Create {
268 cidr: Some("10.0.0.0/16".to_string()),
269 availability_zones: 2,
270 })
271 .build();
272
273 let network2 = Network::new("default-network".to_string())
274 .settings(NetworkSettings::Create {
275 cidr: Some("10.0.0.0/16".to_string()),
276 availability_zones: 3,
277 })
278 .build();
279
280 let error = network1
281 .validate_update(&network2)
282 .expect_err("changing the zone count should be rejected");
283 assert_eq!(error.code, "INVALID_RESOURCE_UPDATE");
284 assert!(error.message.contains("(2 to 3)"), "{}", error.message);
285 }
286
287 #[test]
288 fn test_network_validate_update_different_type_fails() {
289 let network1 = Network::new("default-network".to_string())
290 .settings(NetworkSettings::Create {
291 cidr: Some("10.0.0.0/16".to_string()),
292 availability_zones: 2,
293 })
294 .build();
295
296 let network2 = Network::new("default-network".to_string())
297 .settings(NetworkSettings::ByoVpcAws {
298 vpc_id: "vpc-12345".to_string(),
299 public_subnet_ids: vec![],
300 private_subnet_ids: vec![],
301 security_group_ids: vec![],
302 })
303 .build();
304
305 let result = network1.validate_update(&network2);
307 assert!(result.is_err());
308 }
309
310 #[test]
311 fn test_network_validate_update_use_default() {
312 let network1 = Network::new("default-network".to_string())
313 .settings(NetworkSettings::UseDefault)
314 .build();
315
316 let network2 = Network::new("default-network".to_string())
317 .settings(NetworkSettings::UseDefault)
318 .build();
319
320 assert!(network1.validate_update(&network2).is_ok());
321 }
322
323 #[test]
324 fn test_network_validate_update_use_default_to_create_fails() {
325 let network1 = Network::new("default-network".to_string())
326 .settings(NetworkSettings::UseDefault)
327 .build();
328
329 let network2 = Network::new("default-network".to_string())
330 .settings(NetworkSettings::Create {
331 cidr: None,
332 availability_zones: 2,
333 })
334 .build();
335
336 assert!(network1.validate_update(&network2).is_err());
337 }
338
339 #[test]
340 fn test_network_serialization() {
341 let network = Network::new("default-network".to_string())
342 .settings(NetworkSettings::Create {
343 cidr: None,
344 availability_zones: 2,
345 })
346 .build();
347
348 let json = serde_json::to_string(&network).unwrap();
349 let deserialized: Network = serde_json::from_str(&json).unwrap();
350 assert_eq!(network, deserialized);
351 }
352}