use crate::error::{ErrorData, Result};
use crate::resource::{ResourceDefinition, ResourceOutputsDefinition, ResourceRef};
use crate::stack_settings::NetworkSettings;
use crate::{ResourceType, Stack};
use alien_error::AlienError;
use bon::Builder;
use serde::{Deserialize, Serialize};
use std::any::Any;
use std::fmt::Debug;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct AwsVpcFlowLogSettings {
pub traffic_type: &'static str,
pub log_destination_type: &'static str,
pub max_aggregation_interval: u32,
pub retention_in_days: i32,
pub delivery_service: &'static str,
pub delivery_actions: &'static [&'static str],
}
pub const AWS_VPC_FLOW_LOG: AwsVpcFlowLogSettings = AwsVpcFlowLogSettings {
traffic_type: "ALL",
log_destination_type: "cloud-watch-logs",
max_aggregation_interval: 600,
retention_in_days: 365,
delivery_service: "vpc-flow-logs.amazonaws.com",
delivery_actions: &[
"logs:CreateLogStream",
"logs:PutLogEvents",
"logs:DescribeLogGroups",
"logs:DescribeLogStreams",
],
};
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Builder)]
#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
#[builder(start_fn = new)]
pub struct Network {
#[builder(start_fn)]
pub id: String,
pub settings: NetworkSettings,
}
impl Network {
pub const RESOURCE_TYPE: ResourceType = ResourceType::from_static("network");
pub fn id(&self) -> &str {
&self.id
}
pub fn preflight_settings(stack: &Stack) -> Option<NetworkSettings> {
stack
.resources()
.find_map(|(_, entry)| entry.config.downcast_ref::<Network>())
.filter(|network| matches!(network.settings, NetworkSettings::Create { .. }))
.map(|network| network.settings.clone())
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
#[serde(rename_all = "camelCase")]
pub struct NetworkOutputs {
pub network_id: String,
pub availability_zones: u8,
pub has_public_subnets: bool,
pub has_nat_gateway: bool,
#[serde(default)]
pub egress_public_ips: Vec<String>,
pub cidr: Option<String>,
}
impl ResourceOutputsDefinition for NetworkOutputs {
fn get_resource_type(&self) -> ResourceType {
Network::RESOURCE_TYPE.clone()
}
fn as_any(&self) -> &dyn Any {
self
}
fn box_clone(&self) -> Box<dyn ResourceOutputsDefinition> {
Box::new(self.clone())
}
fn outputs_eq(&self, other: &dyn ResourceOutputsDefinition) -> bool {
other.as_any().downcast_ref::<NetworkOutputs>() == Some(self)
}
fn to_json_value(&self) -> serde_json::Result<serde_json::Value> {
serde_json::to_value(self)
}
}
impl ResourceDefinition for Network {
fn get_resource_type(&self) -> ResourceType {
Self::RESOURCE_TYPE
}
fn id(&self) -> &str {
&self.id
}
fn get_dependencies(&self) -> Vec<ResourceRef> {
Vec::new() }
fn validate_update(&self, new_config: &dyn ResourceDefinition) -> Result<()> {
let new_network = new_config
.as_any()
.downcast_ref::<Network>()
.ok_or_else(|| {
AlienError::new(ErrorData::UnexpectedResourceType {
resource_id: self.id.clone(),
expected: Self::RESOURCE_TYPE,
actual: new_config.get_resource_type(),
})
})?;
if self.id != new_network.id {
return Err(AlienError::new(ErrorData::InvalidResourceUpdate {
resource_id: self.id.clone(),
reason: "the 'id' field is immutable".to_string(),
}));
}
match (&self.settings, &new_network.settings) {
(NetworkSettings::UseDefault, NetworkSettings::UseDefault) => Ok(()),
(
NetworkSettings::Create {
availability_zones: old_zones,
..
},
NetworkSettings::Create {
availability_zones: new_zones,
..
},
) => {
if old_zones != new_zones {
return Err(AlienError::new(ErrorData::InvalidResourceUpdate {
resource_id: self.id.clone(),
reason: format!(
"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"
),
}));
}
Ok(())
}
(NetworkSettings::ByoVpcAws { .. }, NetworkSettings::ByoVpcAws { .. }) => Ok(()),
(NetworkSettings::ByoVpcGcp { .. }, NetworkSettings::ByoVpcGcp { .. }) => Ok(()),
(NetworkSettings::ByoVnetAzure { .. }, NetworkSettings::ByoVnetAzure { .. }) => Ok(()),
_ => Err(AlienError::new(ErrorData::InvalidResourceUpdate {
resource_id: self.id.clone(),
reason: "cannot change network type (e.g., from 'create' to 'use-default')"
.to_string(),
})),
}
}
fn as_any(&self) -> &dyn Any {
self
}
fn as_any_mut(&mut self) -> &mut dyn Any {
self
}
fn box_clone(&self) -> Box<dyn ResourceDefinition> {
Box::new(self.clone())
}
fn resource_eq(&self, other: &dyn ResourceDefinition) -> bool {
other.as_any().downcast_ref::<Network>() == Some(self)
}
fn to_json_value(&self) -> serde_json::Result<serde_json::Value> {
serde_json::to_value(self)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_network_create_settings() {
let network = Network::new("default-network".to_string())
.settings(NetworkSettings::Create {
cidr: Some("10.0.0.0/16".to_string()),
availability_zones: 3,
})
.build();
assert_eq!(network.id(), "default-network");
match &network.settings {
NetworkSettings::Create {
cidr,
availability_zones,
} => {
assert_eq!(cidr.as_deref(), Some("10.0.0.0/16"));
assert_eq!(*availability_zones, 3);
}
_ => panic!("Expected Create settings"),
}
}
#[test]
fn test_network_byo_vpc_aws_settings() {
let network = Network::new("default-network".to_string())
.settings(NetworkSettings::ByoVpcAws {
vpc_id: "vpc-12345".to_string(),
public_subnet_ids: vec!["subnet-pub-1".to_string(), "subnet-pub-2".to_string()],
private_subnet_ids: vec!["subnet-priv-1".to_string(), "subnet-priv-2".to_string()],
security_group_ids: vec!["sg-123".to_string()],
})
.build();
match &network.settings {
NetworkSettings::ByoVpcAws {
vpc_id,
public_subnet_ids,
private_subnet_ids,
security_group_ids,
} => {
assert_eq!(vpc_id, "vpc-12345");
assert_eq!(public_subnet_ids.len(), 2);
assert_eq!(private_subnet_ids.len(), 2);
assert_eq!(security_group_ids.len(), 1);
}
_ => panic!("Expected ByoVpcAws settings"),
}
}
#[test]
fn test_network_validate_update_same_type() {
let network = Network::new("default-network".to_string())
.settings(NetworkSettings::Create {
cidr: Some("10.0.0.0/16".to_string()),
availability_zones: 2,
})
.build();
assert!(network.validate_update(&network.clone()).is_ok());
}
#[test]
fn test_network_validate_update_availability_zone_change_fails() {
let network1 = Network::new("default-network".to_string())
.settings(NetworkSettings::Create {
cidr: Some("10.0.0.0/16".to_string()),
availability_zones: 2,
})
.build();
let network2 = Network::new("default-network".to_string())
.settings(NetworkSettings::Create {
cidr: Some("10.0.0.0/16".to_string()),
availability_zones: 3,
})
.build();
let error = network1
.validate_update(&network2)
.expect_err("changing the zone count should be rejected");
assert_eq!(error.code, "INVALID_RESOURCE_UPDATE");
assert!(error.message.contains("(2 to 3)"), "{}", error.message);
}
#[test]
fn test_network_validate_update_different_type_fails() {
let network1 = Network::new("default-network".to_string())
.settings(NetworkSettings::Create {
cidr: Some("10.0.0.0/16".to_string()),
availability_zones: 2,
})
.build();
let network2 = Network::new("default-network".to_string())
.settings(NetworkSettings::ByoVpcAws {
vpc_id: "vpc-12345".to_string(),
public_subnet_ids: vec![],
private_subnet_ids: vec![],
security_group_ids: vec![],
})
.build();
let result = network1.validate_update(&network2);
assert!(result.is_err());
}
#[test]
fn test_network_validate_update_use_default() {
let network1 = Network::new("default-network".to_string())
.settings(NetworkSettings::UseDefault)
.build();
let network2 = Network::new("default-network".to_string())
.settings(NetworkSettings::UseDefault)
.build();
assert!(network1.validate_update(&network2).is_ok());
}
#[test]
fn test_network_validate_update_use_default_to_create_fails() {
let network1 = Network::new("default-network".to_string())
.settings(NetworkSettings::UseDefault)
.build();
let network2 = Network::new("default-network".to_string())
.settings(NetworkSettings::Create {
cidr: None,
availability_zones: 2,
})
.build();
assert!(network1.validate_update(&network2).is_err());
}
#[test]
fn test_network_serialization() {
let network = Network::new("default-network".to_string())
.settings(NetworkSettings::Create {
cidr: None,
availability_zones: 2,
})
.build();
let json = serde_json::to_string(&network).unwrap();
let deserialized: Network = serde_json::from_str(&json).unwrap();
assert_eq!(network, deserialized);
}
}