diff --git a/sources/Cargo.lock b/sources/Cargo.lock index 45c9f3f9990..f54dd664e26 100644 --- a/sources/Cargo.lock +++ b/sources/Cargo.lock @@ -620,6 +620,34 @@ dependencies = [ "tower", ] +[[package]] +name = "aws-sdk-ec2" +version = "0.24.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b40ee2d853d8300a49513778beb79b1574ff9e9c94b30b1531bc0171d730ad64" +dependencies = [ + "aws-credential-types", + "aws-endpoint", + "aws-http", + "aws-sig-auth", + "aws-smithy-async", + "aws-smithy-client", + "aws-smithy-http", + "aws-smithy-http-tower", + "aws-smithy-json", + "aws-smithy-query", + "aws-smithy-types", + "aws-smithy-xml", + "aws-types", + "bytes", + "fastrand", + "http", + "regex", + "tokio-stream", + "tower", + "tracing", +] + [[package]] name = "aws-sdk-eks" version = "0.24.0" @@ -2855,8 +2883,10 @@ version = "0.1.0" dependencies = [ "apiclient", "aws-config", + "aws-sdk-ec2", "aws-sdk-eks", "aws-smithy-client", + "aws-smithy-types", "aws-types", "bottlerocket-variant", "constants", diff --git a/sources/api/pluto/Cargo.toml b/sources/api/pluto/Cargo.toml index d4588073740..78f404f86d7 100644 --- a/sources/api/pluto/Cargo.toml +++ b/sources/api/pluto/Cargo.toml @@ -19,8 +19,10 @@ imdsclient = { path = "../../imdsclient", version = "0.1" } models = { path = "../../models", version = "0.1" } aws-config = "0.54" aws-sdk-eks = "0.24" +aws-sdk-ec2 = "0.24" aws-types = "0.54" aws-smithy-client = { version = "0.54", default-features = false, features = ["rustls"] } +aws-smithy-types = "0.54" serde_json = "1" snafu = "0.7" tokio = { version = "~1.25", default-features = false, features = ["macros", "rt-multi-thread"] } # LTS diff --git a/sources/api/pluto/src/ec2.rs b/sources/api/pluto/src/ec2.rs new file mode 100644 index 00000000000..1637acadf47 --- /dev/null +++ b/sources/api/pluto/src/ec2.rs @@ -0,0 +1,76 @@ +use crate::proxy; +use aws_smithy_types::error::display::DisplayErrorContext; +use aws_types::region::Region; +use snafu::{OptionExt, ResultExt, Snafu}; +use std::time::Duration; + +// Limit the timeout for the EC2 describe-instances API call to 5 minutes +const EC2_DESCRIBE_INSTANCES_TIMEOUT: Duration = Duration::from_secs(300); + +#[derive(Debug, Snafu)] +pub(super) enum Error { + #[snafu(display( + "Error describing instance '{}': {}", + instance_id, + DisplayErrorContext(source) + ))] + DescribeInstances { + instance_id: String, + source: aws_sdk_eks::types::SdkError, + }, + + #[snafu(display("Timed-out waiting for EC2 DescribeInstances API response: {}", source))] + DescribeInstancesTimeout { source: tokio::time::error::Elapsed }, + + #[snafu(display("Missing field '{}' in EC2 response", field))] + Missing { field: &'static str }, + + #[snafu(context(false), display("{}", source))] + Proxy { source: proxy::Error }, +} + +type Result = std::result::Result; + +pub(super) async fn get_private_dns_name(region: &str, instance_id: &str) -> Result { + // Respect proxy environment variables when making AWS EC2 API requests + let (https_proxy, no_proxy) = proxy::fetch_proxy_env(); + + let config = aws_config::from_env() + .region(Region::new(region.to_owned())) + .load() + .await; + + let client = if let Some(https_proxy) = https_proxy { + let http_client = proxy::setup_http_client(https_proxy, no_proxy)?; + let ec2_config = aws_sdk_ec2::config::Builder::from(&config) + .http_connector(http_client) + .build(); + aws_sdk_ec2::Client::from_conf(ec2_config) + } else { + aws_sdk_ec2::Client::new(&config) + }; + + tokio::time::timeout( + EC2_DESCRIBE_INSTANCES_TIMEOUT, + client + .describe_instances() + .instance_ids(instance_id.to_owned()) + .send(), + ) + .await + .context(DescribeInstancesTimeoutSnafu)? + .context(DescribeInstancesSnafu { instance_id })? + .reservations + .and_then(|reservations| { + reservations.first().and_then(|r| { + r.instances.clone().and_then(|instances| { + instances + .first() + .and_then(|i| i.private_dns_name().map(|s| s.to_string())) + }) + }) + }) + .context(MissingSnafu { + field: "Reservation.Instance.PrivateDNSName", + }) +} diff --git a/sources/api/pluto/src/eks.rs b/sources/api/pluto/src/eks.rs index 6655c7071bf..caea325b35f 100644 --- a/sources/api/pluto/src/eks.rs +++ b/sources/api/pluto/src/eks.rs @@ -1,11 +1,7 @@ +use crate::proxy; use aws_sdk_eks::model::KubernetesNetworkConfigResponse; use aws_types::region::Region; -use hyper::http::uri::InvalidUri; -use hyper::Uri; -use hyper_proxy::{Proxy, ProxyConnector}; -use hyper_rustls::HttpsConnectorBuilder; use snafu::{OptionExt, ResultExt, Snafu}; -use std::env; use std::time::Duration; // Limit the timeout for the EKS describe cluster API call to 5 minutes @@ -26,11 +22,8 @@ pub(super) enum Error { #[snafu(display("Missing field '{}' EKS response", field))] Missing { field: &'static str }, - #[snafu(display("Unable to parse '{}' as URI: {}", input, source))] - UriParse { input: String, source: InvalidUri }, - - #[snafu(display("Failed to create proxy creator: {}", source))] - ProxyConnector { source: std::io::Error }, + #[snafu(context(false), display("{}", source))] + Proxy { source: proxy::Error }, } type Result = std::result::Result; @@ -42,16 +35,7 @@ pub(super) async fn get_cluster_network_config( cluster: &str, ) -> Result { // Respect proxy environment variables when making AWS EKS API requests - let https_proxy = ["https_proxy", "HTTPS_PROXY"] - .iter() - .map(env::var) - .find(|env_var| *env_var != Err(env::VarError::NotPresent)) - .and_then(|s| s.ok()); - let no_proxy = ["no_proxy", "NO_PROXY"] - .iter() - .map(env::var) - .find(|env_var| *env_var != Err(env::VarError::NotPresent)) - .and_then(|s| s.ok()); + let (https_proxy, no_proxy) = proxy::fetch_proxy_env(); let config = aws_config::from_env() .region(Region::new(region.to_owned())) @@ -59,53 +43,7 @@ pub(super) async fn get_cluster_network_config( .await; let client = if let Some(https_proxy) = https_proxy { - // Determines whether a request of a given scheme, host and port should be proxied - // according to `https_proxy` and `no_proxy`. - let intercept = move |scheme: Option<&str>, host: Option<&str>, _port| { - if let Some(host) = host { - if let Some(no_proxy) = &no_proxy { - if scheme != Some("https") { - return false; - } - let no_proxy_hosts: Vec<&str> = no_proxy.split(',').map(|s| s.trim()).collect(); - if no_proxy_hosts.iter().any(|s| *s == "*") { - // Don't proxy anything - return false; - } - // If the host matches one of the no proxy list entries, return false (don't proxy) - // Note that we're not doing anything fancy here for checking `no_proxy` since - // we only expect requests here to be going out to some AWS API endpoint. - return !no_proxy_hosts.iter().any(|no_proxy_host| { - !no_proxy_host.is_empty() && host.ends_with(no_proxy_host) - }); - } - true - } else { - false - } - }; - let mut proxy_uri = https_proxy.parse::().context(UriParseSnafu { - input: &https_proxy, - })?; - // If the proxy's URI doesn't have a scheme, assume HTTP for the scheme and let the proxy - // server forward HTTPS connections and start a tunnel. - if proxy_uri.scheme().is_none() { - proxy_uri = - format!("http://{}", https_proxy) - .parse::() - .context(UriParseSnafu { - input: &https_proxy, - })?; - } - let proxy = Proxy::new(intercept, proxy_uri); - let https_connector = HttpsConnectorBuilder::new() - .with_native_roots() - .https_or_http() - .enable_http2() - .build(); - let proxy_connector = - ProxyConnector::from_proxy(https_connector, proxy).context(ProxyConnectorSnafu)?; - let http_client = aws_smithy_client::hyper_ext::Adapter::builder().build(proxy_connector); + let http_client = proxy::setup_http_client(https_proxy, no_proxy)?; let eks_config = aws_sdk_eks::config::Builder::from(&config) .http_connector(http_client) .build(); diff --git a/sources/api/pluto/src/main.rs b/sources/api/pluto/src/main.rs index 75025ea3715..322274fa15f 100644 --- a/sources/api/pluto/src/main.rs +++ b/sources/api/pluto/src/main.rs @@ -32,7 +32,9 @@ reasonable default is available. */ mod api; +mod ec2; mod eks; +mod proxy; use imdsclient::ImdsClient; use snafu::{ensure, OptionExt, ResultExt}; @@ -51,8 +53,7 @@ const DEFAULT_10_RANGE_DNS_CLUSTER_IP: &str = "172.20.0.10"; const ENI_MAX_PODS_PATH: &str = "/usr/share/eks/eni-max-pods"; mod error { - use crate::api; - use crate::eks; + use crate::{api, ec2, eks}; use snafu::Snafu; use std::net::AddrParseError; @@ -65,6 +66,9 @@ mod error { ))] AwsInfo { source: api::Error }, + #[snafu(display("Missing AWS region"))] + AwsRegion, + #[snafu(display("Missing field '{}' in EKS network config response", field))] MissingNetworkConfig { field: &'static str }, @@ -105,6 +109,9 @@ mod error { #[snafu(display("{}", source))] EksError { source: eks::Error }, + #[snafu(display("{}", source))] + Ec2Error { source: ec2::Error }, + #[snafu(display("Failed to open eni-max-pods file at {}: {}", path, source))] EniMaxPodsFile { path: &'static str, @@ -305,6 +312,24 @@ async fn get_provider_id(client: &mut ImdsClient) -> Result { Ok(format!("aws:///{}/{}", zone, instance_id)) } +async fn get_private_dns_name(client: &mut ImdsClient) -> Result { + let region = api::get_aws_k8s_info() + .await + .context(error::AwsInfoSnafu)? + .region + .context(error::AwsRegionSnafu)?; + let instance_id = client + .fetch_instance_id() + .await + .context(error::ImdsRequestSnafu)? + .context(error::ImdsNoneSnafu { + what: "instance ID", + })?; + ec2::get_private_dns_name(®ion, &instance_id) + .await + .context(error::Ec2Snafu) +} + /// Print usage message. fn usage() -> ! { let program_name = env::args().next().unwrap_or_else(|| "program".to_string()); @@ -333,6 +358,7 @@ async fn run() -> Result<()> { .await .map_err(|_| process::exit(2)), "provider-id" => get_provider_id(&mut client).await, + "private-dns-name" => get_private_dns_name(&mut client).await, _ => usage(), }?; diff --git a/sources/api/pluto/src/proxy.rs b/sources/api/pluto/src/proxy.rs new file mode 100644 index 00000000000..1e355c242a6 --- /dev/null +++ b/sources/api/pluto/src/proxy.rs @@ -0,0 +1,87 @@ +use hyper::Uri; +use hyper_proxy::{Proxy, ProxyConnector}; +use hyper_rustls::HttpsConnectorBuilder; +use snafu::{ResultExt, Snafu}; +use std::env; + +#[derive(Debug, Snafu)] +pub(super) enum Error { + #[snafu(display("Unable to parse '{}' as URI: {}", input, source))] + UriParse { + input: String, + source: hyper::http::uri::InvalidUri, + }, + + #[snafu(display("Failed to create proxy creator: {}", source))] + ProxyConnector { source: std::io::Error }, +} + +type Result = std::result::Result; + +/// Fetches `HTTPS_PROXY` and `NO_PROXY` variables from the process environment. +pub(crate) fn fetch_proxy_env() -> (Option, Option) { + let https_proxy = ["https_proxy", "HTTPS_PROXY"] + .iter() + .map(env::var) + .find(|env_var| *env_var != Err(env::VarError::NotPresent)) + .and_then(|s| s.ok()); + let no_proxy = ["no_proxy", "NO_PROXY"] + .iter() + .map(env::var) + .find(|env_var| *env_var != Err(env::VarError::NotPresent)) + .and_then(|s| s.ok()); + (https_proxy, no_proxy) +} + +/// Setups a hyper-based HTTP client configured with a proxy connector. +pub(crate) fn setup_http_client( + https_proxy: String, + no_proxy: Option, +) -> Result> { + // Determines whether a request of a given scheme, host and port should be proxied + // according to `https_proxy` and `no_proxy`. + let intercept = move |scheme: Option<&str>, host: Option<&str>, _port| { + if let Some(host) = host { + if let Some(no_proxy) = &no_proxy { + if scheme != Some("https") { + return false; + } + let no_proxy_hosts: Vec<&str> = no_proxy.split(',').map(|s| s.trim()).collect(); + if no_proxy_hosts.iter().any(|s| *s == "*") { + // Don't proxy anything + return false; + } + // If the host matches one of the no proxy list entries, return false (don't proxy) + // Note that we're not doing anything fancy here for checking `no_proxy` since + // we only expect requests here to be going out to some AWS API endpoint. + return !no_proxy_hosts.iter().any(|no_proxy_host| { + !no_proxy_host.is_empty() && host.ends_with(no_proxy_host) + }); + } + true + } else { + false + } + }; + let mut proxy_uri = https_proxy.parse::().context(UriParseSnafu { + input: &https_proxy, + })?; + // If the proxy's URI doesn't have a scheme, assume HTTP for the scheme and let the proxy + // server forward HTTPS connections and start a tunnel. + if proxy_uri.scheme().is_none() { + proxy_uri = format!("http://{}", https_proxy) + .parse::() + .context(UriParseSnafu { + input: &https_proxy, + })?; + } + let proxy = Proxy::new(intercept, proxy_uri); + let https_connector = HttpsConnectorBuilder::new() + .with_native_roots() + .https_or_http() + .enable_http2() + .build(); + let proxy_connector = + ProxyConnector::from_proxy(https_connector, proxy).context(ProxyConnectorSnafu)?; + Ok(aws_smithy_client::hyper_ext::Adapter::builder().build(proxy_connector)) +}