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 (NetworkSettings::Create { .. }, NetworkSettings::Create { .. }) => Ok(()),
147 (NetworkSettings::ByoVpcAws { .. }, NetworkSettings::ByoVpcAws { .. }) => Ok(()),
148 (NetworkSettings::ByoVpcGcp { .. }, NetworkSettings::ByoVpcGcp { .. }) => Ok(()),
149 (NetworkSettings::ByoVnetAzure { .. }, NetworkSettings::ByoVnetAzure { .. }) => Ok(()),
150 _ => Err(AlienError::new(ErrorData::InvalidResourceUpdate {
151 resource_id: self.id.clone(),
152 reason: "cannot change network type (e.g., from 'create' to 'use-default')"
153 .to_string(),
154 })),
155 }
156 }
157
158 fn as_any(&self) -> &dyn Any {
159 self
160 }
161
162 fn as_any_mut(&mut self) -> &mut dyn Any {
163 self
164 }
165
166 fn box_clone(&self) -> Box<dyn ResourceDefinition> {
167 Box::new(self.clone())
168 }
169
170 fn resource_eq(&self, other: &dyn ResourceDefinition) -> bool {
171 other.as_any().downcast_ref::<Network>() == Some(self)
172 }
173
174 fn to_json_value(&self) -> serde_json::Result<serde_json::Value> {
175 serde_json::to_value(self)
176 }
177}
178
179#[cfg(test)]
180mod tests {
181 use super::*;
182
183 #[test]
184 fn test_network_create_settings() {
185 let network = Network::new("default-network".to_string())
186 .settings(NetworkSettings::Create {
187 cidr: Some("10.0.0.0/16".to_string()),
188 availability_zones: 3,
189 })
190 .build();
191
192 assert_eq!(network.id(), "default-network");
193 match &network.settings {
194 NetworkSettings::Create {
195 cidr,
196 availability_zones,
197 } => {
198 assert_eq!(cidr.as_deref(), Some("10.0.0.0/16"));
199 assert_eq!(*availability_zones, 3);
200 }
201 _ => panic!("Expected Create settings"),
202 }
203 }
204
205 #[test]
206 fn test_network_byo_vpc_aws_settings() {
207 let network = Network::new("default-network".to_string())
208 .settings(NetworkSettings::ByoVpcAws {
209 vpc_id: "vpc-12345".to_string(),
210 public_subnet_ids: vec!["subnet-pub-1".to_string(), "subnet-pub-2".to_string()],
211 private_subnet_ids: vec!["subnet-priv-1".to_string(), "subnet-priv-2".to_string()],
212 security_group_ids: vec!["sg-123".to_string()],
213 })
214 .build();
215
216 match &network.settings {
217 NetworkSettings::ByoVpcAws {
218 vpc_id,
219 public_subnet_ids,
220 private_subnet_ids,
221 security_group_ids,
222 } => {
223 assert_eq!(vpc_id, "vpc-12345");
224 assert_eq!(public_subnet_ids.len(), 2);
225 assert_eq!(private_subnet_ids.len(), 2);
226 assert_eq!(security_group_ids.len(), 1);
227 }
228 _ => panic!("Expected ByoVpcAws settings"),
229 }
230 }
231
232 #[test]
233 fn test_network_validate_update_same_type() {
234 let network1 = Network::new("default-network".to_string())
235 .settings(NetworkSettings::Create {
236 cidr: Some("10.0.0.0/16".to_string()),
237 availability_zones: 2,
238 })
239 .build();
240
241 let network2 = Network::new("default-network".to_string())
242 .settings(NetworkSettings::Create {
243 cidr: Some("10.0.0.0/16".to_string()),
244 availability_zones: 3, })
246 .build();
247
248 assert!(network1.validate_update(&network2).is_ok());
250 }
251
252 #[test]
253 fn test_network_validate_update_different_type_fails() {
254 let network1 = 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 let network2 = Network::new("default-network".to_string())
262 .settings(NetworkSettings::ByoVpcAws {
263 vpc_id: "vpc-12345".to_string(),
264 public_subnet_ids: vec![],
265 private_subnet_ids: vec![],
266 security_group_ids: vec![],
267 })
268 .build();
269
270 let result = network1.validate_update(&network2);
272 assert!(result.is_err());
273 }
274
275 #[test]
276 fn test_network_validate_update_use_default() {
277 let network1 = Network::new("default-network".to_string())
278 .settings(NetworkSettings::UseDefault)
279 .build();
280
281 let network2 = Network::new("default-network".to_string())
282 .settings(NetworkSettings::UseDefault)
283 .build();
284
285 assert!(network1.validate_update(&network2).is_ok());
286 }
287
288 #[test]
289 fn test_network_validate_update_use_default_to_create_fails() {
290 let network1 = Network::new("default-network".to_string())
291 .settings(NetworkSettings::UseDefault)
292 .build();
293
294 let network2 = Network::new("default-network".to_string())
295 .settings(NetworkSettings::Create {
296 cidr: None,
297 availability_zones: 2,
298 })
299 .build();
300
301 assert!(network1.validate_update(&network2).is_err());
302 }
303
304 #[test]
305 fn test_network_serialization() {
306 let network = Network::new("default-network".to_string())
307 .settings(NetworkSettings::Create {
308 cidr: None,
309 availability_zones: 2,
310 })
311 .build();
312
313 let json = serde_json::to_string(&network).unwrap();
314 let deserialized: Network = serde_json::from_str(&json).unwrap();
315 assert_eq!(network, deserialized);
316 }
317}