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, Copy, PartialEq, Eq)]
21pub struct AwsVpcFlowLogSettings {
22 pub traffic_type: &'static str,
24 pub log_destination_type: &'static str,
25 pub max_aggregation_interval: u32,
27 pub retention_in_days: i32,
29 pub delivery_service: &'static str,
31 pub delivery_actions: &'static [&'static str],
33}
34
35pub const AWS_VPC_FLOW_LOG: AwsVpcFlowLogSettings = AwsVpcFlowLogSettings {
36 traffic_type: "ALL",
37 log_destination_type: "cloud-watch-logs",
38 max_aggregation_interval: 600,
39 retention_in_days: 365,
40 delivery_service: "vpc-flow-logs.amazonaws.com",
41 delivery_actions: &[
42 "logs:CreateLogStream",
43 "logs:PutLogEvents",
44 "logs:DescribeLogGroups",
45 "logs:DescribeLogStreams",
46 ],
47};
48
49#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Builder)]
54#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
55#[serde(rename_all = "camelCase", deny_unknown_fields)]
56#[builder(start_fn = new)]
57pub struct Network {
58 #[builder(start_fn)]
60 pub id: String,
61
62 pub settings: NetworkSettings,
64}
65
66impl Network {
67 pub const RESOURCE_TYPE: ResourceType = ResourceType::from_static("network");
69
70 pub fn id(&self) -> &str {
72 &self.id
73 }
74
75 pub fn preflight_settings(stack: &Stack) -> Option<NetworkSettings> {
82 stack
83 .resources()
84 .find_map(|(_, entry)| entry.config.downcast_ref::<Network>())
85 .filter(|network| matches!(network.settings, NetworkSettings::Create { .. }))
86 .map(|network| network.settings.clone())
87 }
88}
89
90#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
96#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
97#[serde(rename_all = "camelCase")]
98pub struct NetworkOutputs {
99 pub network_id: String,
101
102 pub availability_zones: u8,
104
105 pub has_public_subnets: bool,
107
108 pub has_nat_gateway: bool,
110
111 #[serde(default)]
114 pub egress_public_ips: Vec<String>,
115
116 pub cidr: Option<String>,
118}
119
120impl ResourceOutputsDefinition for NetworkOutputs {
121 fn get_resource_type(&self) -> ResourceType {
122 Network::RESOURCE_TYPE.clone()
123 }
124
125 fn as_any(&self) -> &dyn Any {
126 self
127 }
128
129 fn box_clone(&self) -> Box<dyn ResourceOutputsDefinition> {
130 Box::new(self.clone())
131 }
132
133 fn outputs_eq(&self, other: &dyn ResourceOutputsDefinition) -> bool {
134 other.as_any().downcast_ref::<NetworkOutputs>() == Some(self)
135 }
136
137 fn to_json_value(&self) -> serde_json::Result<serde_json::Value> {
138 serde_json::to_value(self)
139 }
140}
141
142impl ResourceDefinition for Network {
143 fn get_resource_type(&self) -> ResourceType {
144 Self::RESOURCE_TYPE
145 }
146
147 fn id(&self) -> &str {
148 &self.id
149 }
150
151 fn get_dependencies(&self) -> Vec<ResourceRef> {
152 Vec::new() }
154
155 fn validate_update(&self, new_config: &dyn ResourceDefinition) -> Result<()> {
156 let new_network = new_config
157 .as_any()
158 .downcast_ref::<Network>()
159 .ok_or_else(|| {
160 AlienError::new(ErrorData::UnexpectedResourceType {
161 resource_id: self.id.clone(),
162 expected: Self::RESOURCE_TYPE,
163 actual: new_config.get_resource_type(),
164 })
165 })?;
166
167 if self.id != new_network.id {
168 return Err(AlienError::new(ErrorData::InvalidResourceUpdate {
169 resource_id: self.id.clone(),
170 reason: "the 'id' field is immutable".to_string(),
171 }));
172 }
173
174 match (&self.settings, &new_network.settings) {
177 (NetworkSettings::UseDefault, NetworkSettings::UseDefault) => Ok(()),
178 (
179 NetworkSettings::Create {
180 availability_zones: old_zones,
181 ..
182 },
183 NetworkSettings::Create {
184 availability_zones: new_zones,
185 ..
186 },
187 ) => {
188 if old_zones != new_zones {
190 return Err(AlienError::new(ErrorData::InvalidResourceUpdate {
191 resource_id: self.id.clone(),
192 reason: format!(
193 "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"
194 ),
195 }));
196 }
197 Ok(())
198 }
199 (NetworkSettings::ByoVpcAws { .. }, NetworkSettings::ByoVpcAws { .. }) => Ok(()),
200 (NetworkSettings::ByoVpcGcp { .. }, NetworkSettings::ByoVpcGcp { .. }) => Ok(()),
201 (NetworkSettings::ByoVnetAzure { .. }, NetworkSettings::ByoVnetAzure { .. }) => Ok(()),
202 _ => Err(AlienError::new(ErrorData::InvalidResourceUpdate {
203 resource_id: self.id.clone(),
204 reason: "cannot change network type (e.g., from 'create' to 'use-default')"
205 .to_string(),
206 })),
207 }
208 }
209
210 fn as_any(&self) -> &dyn Any {
211 self
212 }
213
214 fn as_any_mut(&mut self) -> &mut dyn Any {
215 self
216 }
217
218 fn box_clone(&self) -> Box<dyn ResourceDefinition> {
219 Box::new(self.clone())
220 }
221
222 fn resource_eq(&self, other: &dyn ResourceDefinition) -> bool {
223 other.as_any().downcast_ref::<Network>() == Some(self)
224 }
225
226 fn to_json_value(&self) -> serde_json::Result<serde_json::Value> {
227 serde_json::to_value(self)
228 }
229}
230
231#[cfg(test)]
232mod tests {
233 use super::*;
234
235 #[test]
236 fn test_network_create_settings() {
237 let network = Network::new("default-network".to_string())
238 .settings(NetworkSettings::Create {
239 cidr: Some("10.0.0.0/16".to_string()),
240 availability_zones: 3,
241 })
242 .build();
243
244 assert_eq!(network.id(), "default-network");
245 match &network.settings {
246 NetworkSettings::Create {
247 cidr,
248 availability_zones,
249 } => {
250 assert_eq!(cidr.as_deref(), Some("10.0.0.0/16"));
251 assert_eq!(*availability_zones, 3);
252 }
253 _ => panic!("Expected Create settings"),
254 }
255 }
256
257 #[test]
258 fn test_network_byo_vpc_aws_settings() {
259 let network = Network::new("default-network".to_string())
260 .settings(NetworkSettings::ByoVpcAws {
261 vpc_id: "vpc-12345".to_string(),
262 public_subnet_ids: vec!["subnet-pub-1".to_string(), "subnet-pub-2".to_string()],
263 private_subnet_ids: vec!["subnet-priv-1".to_string(), "subnet-priv-2".to_string()],
264 security_group_ids: vec!["sg-123".to_string()],
265 })
266 .build();
267
268 match &network.settings {
269 NetworkSettings::ByoVpcAws {
270 vpc_id,
271 public_subnet_ids,
272 private_subnet_ids,
273 security_group_ids,
274 } => {
275 assert_eq!(vpc_id, "vpc-12345");
276 assert_eq!(public_subnet_ids.len(), 2);
277 assert_eq!(private_subnet_ids.len(), 2);
278 assert_eq!(security_group_ids.len(), 1);
279 }
280 _ => panic!("Expected ByoVpcAws settings"),
281 }
282 }
283
284 #[test]
285 fn test_network_validate_update_same_type() {
286 let network = Network::new("default-network".to_string())
287 .settings(NetworkSettings::Create {
288 cidr: Some("10.0.0.0/16".to_string()),
289 availability_zones: 2,
290 })
291 .build();
292
293 assert!(network.validate_update(&network.clone()).is_ok());
294 }
295
296 #[test]
297 fn test_network_validate_update_availability_zone_change_fails() {
298 let network1 = Network::new("default-network".to_string())
299 .settings(NetworkSettings::Create {
300 cidr: Some("10.0.0.0/16".to_string()),
301 availability_zones: 2,
302 })
303 .build();
304
305 let network2 = Network::new("default-network".to_string())
306 .settings(NetworkSettings::Create {
307 cidr: Some("10.0.0.0/16".to_string()),
308 availability_zones: 3,
309 })
310 .build();
311
312 let error = network1
313 .validate_update(&network2)
314 .expect_err("changing the zone count should be rejected");
315 assert_eq!(error.code, "INVALID_RESOURCE_UPDATE");
316 assert!(error.message.contains("(2 to 3)"), "{}", error.message);
317 }
318
319 #[test]
320 fn test_network_validate_update_different_type_fails() {
321 let network1 = Network::new("default-network".to_string())
322 .settings(NetworkSettings::Create {
323 cidr: Some("10.0.0.0/16".to_string()),
324 availability_zones: 2,
325 })
326 .build();
327
328 let network2 = Network::new("default-network".to_string())
329 .settings(NetworkSettings::ByoVpcAws {
330 vpc_id: "vpc-12345".to_string(),
331 public_subnet_ids: vec![],
332 private_subnet_ids: vec![],
333 security_group_ids: vec![],
334 })
335 .build();
336
337 let result = network1.validate_update(&network2);
339 assert!(result.is_err());
340 }
341
342 #[test]
343 fn test_network_validate_update_use_default() {
344 let network1 = Network::new("default-network".to_string())
345 .settings(NetworkSettings::UseDefault)
346 .build();
347
348 let network2 = Network::new("default-network".to_string())
349 .settings(NetworkSettings::UseDefault)
350 .build();
351
352 assert!(network1.validate_update(&network2).is_ok());
353 }
354
355 #[test]
356 fn test_network_validate_update_use_default_to_create_fails() {
357 let network1 = Network::new("default-network".to_string())
358 .settings(NetworkSettings::UseDefault)
359 .build();
360
361 let network2 = Network::new("default-network".to_string())
362 .settings(NetworkSettings::Create {
363 cidr: None,
364 availability_zones: 2,
365 })
366 .build();
367
368 assert!(network1.validate_update(&network2).is_err());
369 }
370
371 #[test]
372 fn test_network_serialization() {
373 let network = Network::new("default-network".to_string())
374 .settings(NetworkSettings::Create {
375 cidr: None,
376 availability_zones: 2,
377 })
378 .build();
379
380 let json = serde_json::to_string(&network).unwrap();
381 let deserialized: Network = serde_json::from_str(&json).unwrap();
382 assert_eq!(network, deserialized);
383 }
384}