diff --git a/docs/release-notes.md b/docs/release-notes.md index aedccfef..dc91cb9f 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -13,6 +13,7 @@ Major changes: Minor changes: - Hetzner: fix duplicate attribute prefix +- Hetzner: Return private IPv4 addresses in attributes - Fix Azure SSH key fetching when no key provisioned Packaging changes: diff --git a/docs/usage/attributes.md b/docs/usage/attributes.md index 42e5a41a..cf7d14bf 100644 --- a/docs/usage/attributes.md +++ b/docs/usage/attributes.md @@ -98,6 +98,7 @@ Cloud providers with supported metadata endpoints and their respective attribute - AFTERBURN_HETZNER_HOSTNAME - AFTERBURN_HETZNER_INSTANCE_ID - AFTERBURN_HETZNER_PUBLIC_IPV4 + - AFTERBURN_HETZNER_PRIVATE_IPV4_0 - AFTERBURN_HETZNER_REGION * ibmcloud - AFTERBURN_IBMCLOUD_INSTANCE_ID diff --git a/src/providers/hetzner/mock_tests.rs b/src/providers/hetzner/mock_tests.rs index 79a16622..443c7027 100644 --- a/src/providers/hetzner/mock_tests.rs +++ b/src/providers/hetzner/mock_tests.rs @@ -14,7 +14,8 @@ fn setup() -> (mockito::ServerGuard, HetznerProvider) { #[test] fn test_attributes() { - let endpoint = "/hetzner/v1/metadata"; + let endpoint_metadata = "/hetzner/v1/metadata"; + let endpoint_networks = "/hetzner/v1/metadata/private-networks"; let (mut server, provider) = setup(); let availability_zone = "fsn1-dc14"; @@ -23,7 +24,7 @@ fn test_attributes() { let public_ipv4 = "192.0.2.10"; let region = "eu-central"; - let body = format!( + let body_metadata = format!( r#"availability-zone: {availability_zone} hostname: {hostname} instance-id: {instance_id} @@ -34,30 +35,60 @@ public-keys: [] vendor_data: "blah blah blah""# ); + let ip_0 = "10.0.0.2"; + let ip_1 = "10.128.0.2"; + + let body_networks = format!( + r#"- ip: {ip_0} +- ip: {ip_1}"# + ); + let expected = maplit::hashmap! { "HETZNER_AVAILABILITY_ZONE".to_string() => availability_zone.to_string(), "HETZNER_HOSTNAME".to_string() => hostname.to_string(), "HETZNER_INSTANCE_ID".to_string() => instance_id.to_string(), "HETZNER_PUBLIC_IPV4".to_string() => public_ipv4.to_string(), "HETZNER_REGION".to_string() => region.to_string(), + "HETZNER_PRIVATE_IPV4_0".to_string() => ip_0.to_string(), + "HETZNER_PRIVATE_IPV4_1".to_string() => ip_1.to_string(), }; // Fail on not found provider.attributes().unwrap_err(); - // Fail on internal server errors - let mock = server.mock("GET", endpoint).with_status(503).create(); + // Fail on internal server errors (metadata endpoint) + let mock_metadata = server + .mock("GET", endpoint_metadata) + .with_status(503) + .create(); provider.attributes().unwrap_err(); - mock.assert(); + mock_metadata.assert(); + + let mock_metadata = server + .mock("GET", endpoint_metadata) + .with_status(200) + .with_body(body_metadata) + .expect(2) // Once for the private-networks error test and once to compare the result + .create(); + + // Fail on internal server errors (networks endpoint) + let mock_networks = server + .mock("GET", endpoint_networks) + .with_status(503) + .create(); + provider.attributes().unwrap_err(); + mock_networks.assert(); // Fetch metadata - let mock = server - .mock("GET", endpoint) + let mock_networks = server + .mock("GET", endpoint_networks) .with_status(200) - .with_body(body) + .with_body(body_networks) .create(); + let actual = provider.attributes().unwrap(); - mock.assert(); + mock_metadata.assert(); + mock_networks.assert(); assert_eq!(actual, expected); } diff --git a/src/providers/hetzner/mod.rs b/src/providers/hetzner/mod.rs index 58f98465..6bb1d6ea 100644 --- a/src/providers/hetzner/mod.rs +++ b/src/providers/hetzner/mod.rs @@ -50,14 +50,24 @@ impl HetznerProvider { } impl MetadataProvider for HetznerProvider { - fn attributes(&self) -> Result> { - let meta: HetznerMetadata = self + fn attributes(&self) -> Result> { + let metadata: Metadata = self .client .get(retry::Yaml, HETZNER_METADATA_BASE_URL.to_string()) .send()? .unwrap(); - Ok(meta.into()) + let private_networks: Vec = self + .client + .get(retry::Yaml, Self::endpoint_for("private-networks")) + .send()? + .unwrap(); + + Ok(Attributes { + metadata, + private_networks, + } + .into()) } fn hostname(&self) -> Result> { @@ -90,9 +100,14 @@ impl MetadataProvider for HetznerProvider { } } +#[derive(Debug, Deserialize)] +struct PrivateNetwork { + ip: Option, +} + #[derive(Debug, Deserialize)] #[serde(rename_all = "kebab-case")] -struct HetznerMetadata { +struct Metadata { hostname: Option, instance_id: Option, public_ipv4: Option, @@ -100,8 +115,13 @@ struct HetznerMetadata { region: Option, } -impl From for HashMap { - fn from(meta: HetznerMetadata) -> Self { +struct Attributes { + metadata: Metadata, + private_networks: Vec, +} + +impl From for HashMap { + fn from(attributes: Attributes) -> Self { let mut out = HashMap::with_capacity(5); let add_value = |map: &mut HashMap<_, _>, key: &str, value: Option| { @@ -113,16 +133,28 @@ impl From for HashMap { add_value( &mut out, "HETZNER_AVAILABILITY_ZONE", - meta.availability_zone, + attributes.metadata.availability_zone, ); - add_value(&mut out, "HETZNER_HOSTNAME", meta.hostname); + add_value(&mut out, "HETZNER_HOSTNAME", attributes.metadata.hostname); add_value( &mut out, "HETZNER_INSTANCE_ID", - meta.instance_id.map(|i| i.to_string()), + attributes.metadata.instance_id.map(|i| i.to_string()), + ); + add_value( + &mut out, + "HETZNER_PUBLIC_IPV4", + attributes.metadata.public_ipv4, ); - add_value(&mut out, "HETZNER_PUBLIC_IPV4", meta.public_ipv4); - add_value(&mut out, "HETZNER_REGION", meta.region); + add_value(&mut out, "HETZNER_REGION", attributes.metadata.region); + + for (i, a) in attributes.private_networks.iter().enumerate() { + add_value( + &mut out, + format!("HETZNER_PRIVATE_IPV4_{i}").as_str(), + a.ip.clone(), + ); + } out } @@ -130,7 +162,7 @@ impl From for HashMap { #[cfg(test)] mod tests { - use super::HetznerMetadata; + use super::{Metadata, PrivateNetwork}; #[test] fn test_metadata_deserialize() { @@ -141,11 +173,39 @@ public-ipv4: 1.2.3.4 region: eu-central public-keys: []"#; - let meta: HetznerMetadata = serde_yaml::from_str(body).unwrap(); + let meta: Metadata = serde_yaml::from_str(body).unwrap(); assert_eq!(meta.availability_zone.unwrap(), "hel1-dc2"); assert_eq!(meta.hostname.unwrap(), "my-server"); assert_eq!(meta.instance_id.unwrap(), 42); assert_eq!(meta.public_ipv4.unwrap(), "1.2.3.4"); } + + #[test] + fn test_private_networks_deserialize() { + let body = r"- ip: 10.0.0.2 + alias_ips: [] + interface_num: 2 + mac_address: 86:00:00:98:40:6e + network_id: 4124728 + network_name: foo + network: 10.0.0.0/16 + subnet: 10.0.0.0/24 + gateway: 10.0.0.1 +- ip: 10.128.0.2 + alias_ips: [] + interface_num: 1 + mac_address: 86:00:00:98:40:6d + network_id: 4451335 + network_name: bar + network: 10.128.0.0/16 + subnet: 10.128.0.0/16 + gateway: 10.128.0.1"; + + let private_networks: Vec = serde_yaml::from_str(body).unwrap(); + + assert_eq!(private_networks.len(), 2); + assert_eq!(private_networks[0].ip.clone().unwrap(), "10.0.0.2"); + assert_eq!(private_networks[1].ip.clone().unwrap(), "10.128.0.2"); + } }