Skip to content

Commit 2f694d9

Browse files
authored
Merge pull request #734 from jpculp/cluster-dns-ipv6
pluto: improve cluster DNS IP generation
2 parents 14f16fa + dee7b96 commit 2f694d9

File tree

3 files changed

+86
-9
lines changed

3 files changed

+86
-9
lines changed

sources/Cargo.lock

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

sources/api/pluto/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,3 +38,4 @@ generate-readme.workspace = true
3838

3939
[dev-dependencies]
4040
httptest.workspace = true
41+
test-case.workspace = true

sources/api/pluto/src/main.rs

Lines changed: 84 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -37,14 +37,15 @@ mod ec2;
3737
mod eks;
3838

3939
use api::{settings_view_get, settings_view_set, SettingsViewDelta};
40+
use aws_sdk_eks::types::IpFamily;
4041
use aws_smithy_experimental::hyper_1_0::CryptoMode;
4142
use base64::Engine;
4243
use bottlerocket_modeled_types::{KubernetesClusterDnsIp, KubernetesHostnameOverrideSource};
4344
use imdsclient::ImdsClient;
4445
use snafu::{ensure, OptionExt, ResultExt};
4546
use std::fs::File;
4647
use std::io::{BufRead, BufReader, Write};
47-
use std::net::IpAddr;
48+
use std::net::{IpAddr, Ipv6Addr};
4849
use std::path::Path;
4950
use std::str::FromStr;
5051
use std::string::String;
@@ -220,10 +221,10 @@ async fn get_max_pods_from_file(instance_type: &str, pods_file: &'static str) ->
220221

221222
/// Returns the cluster's DNS address.
222223
///
223-
/// For IPv4 clusters, first it attempts to call EKS describe-cluster to find the `serviceIpv4Cidr`.
224+
/// First attempts to call EKS describe-cluster to find the `ipFamily` and its corresponding CIDR.
224225
/// If that works, it returns the expected cluster DNS IP address which is obtained by substituting
225226
/// `10` for the last octet. If the EKS call is not successful, it falls back to using IMDS MAC CIDR
226-
/// blocks to return one of two default addresses.
227+
/// blocks to return one of two default IPv4 addresses.
227228
async fn generate_cluster_dns_ip(
228229
client: &mut ImdsClient,
229230
aws_k8s_info: &mut SettingsViewDelta,
@@ -267,17 +268,27 @@ async fn get_eks_network_config(aws_k8s_info: &SettingsViewDelta) -> Result<Opti
267268
.await
268269
.context(error::EksSnafu)
269270
{
270-
// Derive cluster-dns-ip from the service IPv4 CIDR
271-
if let Some(ipv4_cidr) = config.service_ipv4_cidr {
272-
if let Ok(dns_ip) = get_dns_from_ipv4_cidr(&ipv4_cidr) {
273-
return Ok(Some(dns_ip));
274-
}
275-
}
271+
return Ok(get_dns_from_cluster_config(&config));
276272
}
277273
}
278274
Ok(None)
279275
}
280276

277+
/// Gets the DNS IP address based on the kubernetes network configuration for
278+
/// the EKS Cluster
279+
fn get_dns_from_cluster_config(config: &eks::ClusterNetworkConfig) -> Option<String> {
280+
match config.ip_family {
281+
Some(IpFamily::Ipv6) => config
282+
.service_ipv6_cidr
283+
.as_ref()
284+
.and_then(|cidr| get_dns_from_ipv6_cidr(cidr).ok()),
285+
_ => config
286+
.service_ipv4_cidr
287+
.as_ref()
288+
.and_then(|cidr| get_dns_from_ipv4_cidr(cidr).ok()),
289+
}
290+
}
291+
281292
/// Replicates [this] logic from the EKS AMI:
282293
///
283294
/// ```sh
@@ -296,6 +307,29 @@ fn get_dns_from_ipv4_cidr(cidr: &str) -> Result<String> {
296307
split[3] = "10";
297308
Ok(split.join("."))
298309
}
310+
/// Replicates [this] logic from the EKS AMI:
311+
///
312+
/// ```sh
313+
/// DNS_CLUSTER_IP=$(awk -F/ '{print $1}' <<< $SERVICE_IPV6_CIDR)a
314+
/// ```
315+
/// [this]: https://github.com/awslabs/amazon-eks-ami/blob/0f5c129/templates/al2/runtime/bootstrap.sh#L463
316+
fn get_dns_from_ipv6_cidr(cidr: &str) -> Result<String> {
317+
// Extract the address part before the slash
318+
let addr_str = cidr.split('/').next().context(error::CidrParseSnafu {
319+
cidr,
320+
reason: "missing address component",
321+
})?;
322+
323+
let base_addr: Ipv6Addr = addr_str
324+
.parse()
325+
.context(error::BadIpSnafu { ip: addr_str })?;
326+
327+
// Set the last segment to 0xa (10 in hex)
328+
let mut segments = base_addr.segments();
329+
segments[7] = 0xa;
330+
331+
Ok(Ipv6Addr::from(segments).to_string())
332+
}
299333

300334
/// Gets gets the the first VPC IPV4 CIDR block from IMDS. If it starts with `10`, returns
301335
/// `10.100.0.10`, otherwise returns `172.20.0.10`
@@ -538,9 +572,11 @@ mod test {
538572
use super::*;
539573
use crate::api::SettingsViewDelta;
540574
use api::SettingsView;
575+
use aws_sdk_eks::types::KubernetesNetworkConfigResponse;
541576
use bottlerocket_modeled_types::ValidBase64;
542577
use bottlerocket_settings_models::AwsSettingsV1;
543578
use httptest::{matchers::*, responders::*, Expectation, Server};
579+
use test_case::test_case;
544580

545581
#[test]
546582
fn test_get_dns_from_cidr_ok() {
@@ -557,6 +593,23 @@ mod test {
557593
assert!(result.is_err());
558594
}
559595

596+
#[test_case("fd00::/108", "fd00::a" ; "compressed ULA - common in EKS")]
597+
#[test_case("2001:db8::/108", "2001:db8::a" ; "compressed middle")]
598+
#[test_case("2001:db8:1234::/108", "2001:db8:1234::a" ; "partially expanded")]
599+
#[test_case("2001:db8:1234:5678::/108", "2001:db8:1234:5678::a" ; "fully expanded")]
600+
#[test_case("fe80::/108", "fe80::a" ; "link local")]
601+
fn test_get_dns_from_ipv6_cidr_ok(input: &str, expected: &str) {
602+
let actual = get_dns_from_ipv6_cidr(input).unwrap();
603+
assert_eq!(expected, actual);
604+
}
605+
606+
#[test_case("null" ; "no slash")]
607+
#[test_case("" ; "empty string")]
608+
fn test_get_dns_from_ipv6_cidr_err(input: &str) {
609+
let result = get_dns_from_ipv6_cidr(input);
610+
assert!(result.is_err());
611+
}
612+
560613
// Because of test parallelization, serialize the AWS config tests such that
561614
// the AWS_CONFIG_FILE env variable is deterministically set or unset.
562615
#[test]
@@ -609,6 +662,28 @@ mod test {
609662
assert!(env::var(AWS_CONFIG_FILE_ENV_VAR).is_err()); // NotPresent
610663
}
611664

665+
#[test_case(Some(IpFamily::Ipv4), Some("10.100.0.0/16"), None, Some("10.100.0.10") ; "ipv4 with valid cidr")]
666+
#[test_case(Some(IpFamily::Ipv6), None, Some("2001:db8:1234::/108"), Some("2001:db8:1234::a") ; "ipv6 with valid cidr")]
667+
#[test_case(None, Some("172.20.0.0/16"), None, Some("172.20.0.10") ; "default ipv4 family with valid cidr")]
668+
#[test_case(Some(IpFamily::Ipv4), None, None, None ; "missing ipv4 cidr")]
669+
#[test_case(Some(IpFamily::Ipv6), None, None, None ; "missing ipv6 cidr")]
670+
#[test_case(Some(IpFamily::Ipv4), Some("invalid"), None, None ; "invalid ipv4 cidr")]
671+
#[test_case(Some(IpFamily::Ipv6), None, Some("invalid"), None ; "invalid ipv6 cidr")]
672+
fn test_dns_from_cluster_config(
673+
ip_family: Option<IpFamily>,
674+
service_ipv4_cidr: Option<&str>,
675+
service_ipv6_cidr: Option<&str>,
676+
expected: Option<&str>,
677+
) {
678+
let config = KubernetesNetworkConfigResponse::builder()
679+
.set_ip_family(ip_family)
680+
.set_service_ipv4_cidr(service_ipv4_cidr.map(String::from))
681+
.set_service_ipv6_cidr(service_ipv6_cidr.map(String::from))
682+
.build();
683+
let result = get_dns_from_cluster_config(&config);
684+
assert_eq!(result.as_deref(), expected);
685+
}
686+
612687
#[tokio::test]
613688
async fn test_hostname_override_source() {
614689
let server = Server::run();

0 commit comments

Comments
 (0)