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    /// Public addresses used by the deployment's NAT. Empty when unknown, including
66    /// bring-your-own networks whose egress is managed outside this deployment.
67    #[serde(default)]
68    pub egress_public_ips: Vec<String>,
69
70    /// CIDR block (if created by Alien, None for BYO-VPC)
71    pub cidr: Option<String>,
72}
73
74impl ResourceOutputsDefinition for NetworkOutputs {
75    fn get_resource_type(&self) -> ResourceType {
76        Network::RESOURCE_TYPE.clone()
77    }
78
79    fn as_any(&self) -> &dyn Any {
80        self
81    }
82
83    fn box_clone(&self) -> Box<dyn ResourceOutputsDefinition> {
84        Box::new(self.clone())
85    }
86
87    fn outputs_eq(&self, other: &dyn ResourceOutputsDefinition) -> bool {
88        other.as_any().downcast_ref::<NetworkOutputs>() == Some(self)
89    }
90
91    fn to_json_value(&self) -> serde_json::Result<serde_json::Value> {
92        serde_json::to_value(self)
93    }
94}
95
96impl ResourceDefinition for Network {
97    fn get_resource_type(&self) -> ResourceType {
98        Self::RESOURCE_TYPE
99    }
100
101    fn id(&self) -> &str {
102        &self.id
103    }
104
105    fn get_dependencies(&self) -> Vec<ResourceRef> {
106        Vec::new() // Network has no dependencies
107    }
108
109    fn validate_update(&self, new_config: &dyn ResourceDefinition) -> Result<()> {
110        let new_network = new_config
111            .as_any()
112            .downcast_ref::<Network>()
113            .ok_or_else(|| {
114                AlienError::new(ErrorData::UnexpectedResourceType {
115                    resource_id: self.id.clone(),
116                    expected: Self::RESOURCE_TYPE,
117                    actual: new_config.get_resource_type(),
118                })
119            })?;
120
121        if self.id != new_network.id {
122            return Err(AlienError::new(ErrorData::InvalidResourceUpdate {
123                resource_id: self.id.clone(),
124                reason: "the 'id' field is immutable".to_string(),
125            }));
126        }
127
128        // Validate that network type doesn't change (e.g., from Create to BYO-VPC)
129        // This would require recreating all networking infrastructure
130        match (&self.settings, &new_network.settings) {
131            (NetworkSettings::UseDefault, NetworkSettings::UseDefault) => Ok(()),
132            (NetworkSettings::Create { .. }, NetworkSettings::Create { .. }) => Ok(()),
133            (NetworkSettings::ByoVpcAws { .. }, NetworkSettings::ByoVpcAws { .. }) => Ok(()),
134            (NetworkSettings::ByoVpcGcp { .. }, NetworkSettings::ByoVpcGcp { .. }) => Ok(()),
135            (NetworkSettings::ByoVnetAzure { .. }, NetworkSettings::ByoVnetAzure { .. }) => Ok(()),
136            _ => Err(AlienError::new(ErrorData::InvalidResourceUpdate {
137                resource_id: self.id.clone(),
138                reason: "cannot change network type (e.g., from 'create' to 'use-default')"
139                    .to_string(),
140            })),
141        }
142    }
143
144    fn as_any(&self) -> &dyn Any {
145        self
146    }
147
148    fn as_any_mut(&mut self) -> &mut dyn Any {
149        self
150    }
151
152    fn box_clone(&self) -> Box<dyn ResourceDefinition> {
153        Box::new(self.clone())
154    }
155
156    fn resource_eq(&self, other: &dyn ResourceDefinition) -> bool {
157        other.as_any().downcast_ref::<Network>() == Some(self)
158    }
159
160    fn to_json_value(&self) -> serde_json::Result<serde_json::Value> {
161        serde_json::to_value(self)
162    }
163}
164
165#[cfg(test)]
166mod tests {
167    use super::*;
168
169    #[test]
170    fn test_network_create_settings() {
171        let network = Network::new("default-network".to_string())
172            .settings(NetworkSettings::Create {
173                cidr: Some("10.0.0.0/16".to_string()),
174                availability_zones: 3,
175            })
176            .build();
177
178        assert_eq!(network.id(), "default-network");
179        match &network.settings {
180            NetworkSettings::Create {
181                cidr,
182                availability_zones,
183            } => {
184                assert_eq!(cidr.as_deref(), Some("10.0.0.0/16"));
185                assert_eq!(*availability_zones, 3);
186            }
187            _ => panic!("Expected Create settings"),
188        }
189    }
190
191    #[test]
192    fn test_network_byo_vpc_aws_settings() {
193        let network = Network::new("default-network".to_string())
194            .settings(NetworkSettings::ByoVpcAws {
195                vpc_id: "vpc-12345".to_string(),
196                public_subnet_ids: vec!["subnet-pub-1".to_string(), "subnet-pub-2".to_string()],
197                private_subnet_ids: vec!["subnet-priv-1".to_string(), "subnet-priv-2".to_string()],
198                security_group_ids: vec!["sg-123".to_string()],
199            })
200            .build();
201
202        match &network.settings {
203            NetworkSettings::ByoVpcAws {
204                vpc_id,
205                public_subnet_ids,
206                private_subnet_ids,
207                security_group_ids,
208            } => {
209                assert_eq!(vpc_id, "vpc-12345");
210                assert_eq!(public_subnet_ids.len(), 2);
211                assert_eq!(private_subnet_ids.len(), 2);
212                assert_eq!(security_group_ids.len(), 1);
213            }
214            _ => panic!("Expected ByoVpcAws settings"),
215        }
216    }
217
218    #[test]
219    fn test_network_validate_update_same_type() {
220        let network1 = Network::new("default-network".to_string())
221            .settings(NetworkSettings::Create {
222                cidr: Some("10.0.0.0/16".to_string()),
223                availability_zones: 2,
224            })
225            .build();
226
227        let network2 = Network::new("default-network".to_string())
228            .settings(NetworkSettings::Create {
229                cidr: Some("10.0.0.0/16".to_string()),
230                availability_zones: 3, // Changed
231            })
232            .build();
233
234        // Same network type (Create) should be allowed
235        assert!(network1.validate_update(&network2).is_ok());
236    }
237
238    #[test]
239    fn test_network_validate_update_different_type_fails() {
240        let network1 = Network::new("default-network".to_string())
241            .settings(NetworkSettings::Create {
242                cidr: Some("10.0.0.0/16".to_string()),
243                availability_zones: 2,
244            })
245            .build();
246
247        let network2 = Network::new("default-network".to_string())
248            .settings(NetworkSettings::ByoVpcAws {
249                vpc_id: "vpc-12345".to_string(),
250                public_subnet_ids: vec![],
251                private_subnet_ids: vec![],
252                security_group_ids: vec![],
253            })
254            .build();
255
256        // Changing network type should fail
257        let result = network1.validate_update(&network2);
258        assert!(result.is_err());
259    }
260
261    #[test]
262    fn test_network_validate_update_use_default() {
263        let network1 = Network::new("default-network".to_string())
264            .settings(NetworkSettings::UseDefault)
265            .build();
266
267        let network2 = Network::new("default-network".to_string())
268            .settings(NetworkSettings::UseDefault)
269            .build();
270
271        assert!(network1.validate_update(&network2).is_ok());
272    }
273
274    #[test]
275    fn test_network_validate_update_use_default_to_create_fails() {
276        let network1 = Network::new("default-network".to_string())
277            .settings(NetworkSettings::UseDefault)
278            .build();
279
280        let network2 = Network::new("default-network".to_string())
281            .settings(NetworkSettings::Create {
282                cidr: None,
283                availability_zones: 2,
284            })
285            .build();
286
287        assert!(network1.validate_update(&network2).is_err());
288    }
289
290    #[test]
291    fn test_network_serialization() {
292        let network = Network::new("default-network".to_string())
293            .settings(NetworkSettings::Create {
294                cidr: None,
295                availability_zones: 2,
296            })
297            .build();
298
299        let json = serde_json::to_string(&network).unwrap();
300        let deserialized: Network = serde_json::from_str(&json).unwrap();
301        assert_eq!(network, deserialized);
302    }
303}