Skip to content

Commit

Permalink
pluto: add 'private-dns-name' subcommand
Browse files Browse the repository at this point in the history
Adds a new command for retrieving the instance's PrivateDnsName.

Refactors out code for setting up proxy for the AWS API clients so it
can be shared between the EKS module and EC2 module.
  • Loading branch information
etungsten authored and cbgbt committed Apr 20, 2023
1 parent bdff003 commit cf83e07
Show file tree
Hide file tree
Showing 6 changed files with 228 additions and 69 deletions.
30 changes: 30 additions & 0 deletions sources/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions sources/api/pluto/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
76 changes: 76 additions & 0 deletions sources/api/pluto/src/ec2.rs
Original file line number Diff line number Diff line change
@@ -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<aws_sdk_ec2::error::DescribeInstancesError>,
},

#[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<T> = std::result::Result<T, Error>;

pub(super) async fn get_private_dns_name(region: &str, instance_id: &str) -> Result<String> {
// 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",
})
}
72 changes: 5 additions & 67 deletions sources/api/pluto/src/eks.rs
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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<T> = std::result::Result<T, Error>;
Expand All @@ -42,70 +35,15 @@ pub(super) async fn get_cluster_network_config(
cluster: &str,
) -> Result<ClusterNetworkConfig> {
// 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()))
.load()
.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::<Uri>().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::<Uri>()
.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();
Expand Down
30 changes: 28 additions & 2 deletions sources/api/pluto/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,9 @@ reasonable default is available.
*/

mod api;
mod ec2;
mod eks;
mod proxy;

use imdsclient::ImdsClient;
use snafu::{ensure, OptionExt, ResultExt};
Expand All @@ -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;

Expand All @@ -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 },

Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -305,6 +312,24 @@ async fn get_provider_id(client: &mut ImdsClient) -> Result<String> {
Ok(format!("aws:///{}/{}", zone, instance_id))
}

async fn get_private_dns_name(client: &mut ImdsClient) -> Result<String> {
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(&region, &instance_id)
.await
.context(error::Ec2Snafu)
}

/// Print usage message.
fn usage() -> ! {
let program_name = env::args().next().unwrap_or_else(|| "program".to_string());
Expand Down Expand Up @@ -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(),
}?;

Expand Down
87 changes: 87 additions & 0 deletions sources/api/pluto/src/proxy.rs
Original file line number Diff line number Diff line change
@@ -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<T> = std::result::Result<T, Error>;

/// Fetches `HTTPS_PROXY` and `NO_PROXY` variables from the process environment.
pub(crate) fn fetch_proxy_env() -> (Option<String>, Option<String>) {
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<String>,
) -> Result<impl Into<aws_smithy_client::http_connector::HttpConnector>> {
// 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::<Uri>().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::<Uri>()
.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))
}

0 comments on commit cf83e07

Please sign in to comment.