Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Config: New incluster and incluster_dns constructors #1001

Merged
merged 10 commits into from Sep 9, 2022
2 changes: 1 addition & 1 deletion .devcontainer/devcontainer.json
Expand Up @@ -8,7 +8,7 @@

// These extensions are loaded for all users by default.
"extensions": [
"matklad.rust-analyzer",
"rust-lang.rust-analyzer",
clux marked this conversation as resolved.
Show resolved Hide resolved
"NathanRidley.autotrim",
"samverschueren.final-newline",
"tamasfe.even-better-toml",
Expand Down
109 changes: 108 additions & 1 deletion kube-client/src/config/incluster_config.rs
@@ -1,5 +1,9 @@
use std::env;
use thiserror::Error;

const SERVICE_HOSTENV: &str = "KUBERNETES_SERVICE_HOST";
const SERVICE_PORTENV: &str = "KUBERNETES_SERVICE_PORT";

// Mounted credential files
const SERVICE_TOKENFILE: &str = "/var/run/secrets/kubernetes.io/serviceaccount/token";
const SERVICE_CERTFILE: &str = "/var/run/secrets/kubernetes.io/serviceaccount/ca.crt";
Expand All @@ -12,10 +16,18 @@ pub enum Error {
#[error("failed to read the default namespace: {0}")]
ReadDefaultNamespace(#[source] std::io::Error),

/// Failed to read the in-cluster environment variables
#[error("failed to read an incluster environment variable: {0}")]
ReadEnvironmentVariable(#[source] env::VarError),

/// Failed to read a certificate bundle
#[error("failed to read a certificate bundle: {0}")]
ReadCertificateBundle(#[source] std::io::Error),

/// Failed to parse cluster port value
#[error("failed to parse cluster port: {0}")]
ParseClusterPort(#[source] std::num::ParseIntError),

/// Failed to parse cluster url
#[error("failed to parse cluster url: {0}")]
ParseClusterUrl(#[source] http::uri::InvalidUri),
Expand All @@ -25,10 +37,56 @@ pub enum Error {
ParseCertificates(#[source] pem::PemError),
}

pub fn kube_dns() -> http::Uri {
/// Returns the URI of the Kubernetes API server using the in-cluster DNS name
/// `kubernetes.default.svc`.
pub(super) fn kube_dns() -> http::Uri {
http::Uri::from_static("https://kubernetes.default.svc/")
}

/// Returns the URI of the Kubernetes API server by reading the
/// `KUBERNETES_SERVICE_HOST` and `KUBERNETES_SERVICE_PORT` environment
/// variables.
pub(super) fn try_kube_from_env() -> Result<http::Uri, Error> {
// client-go requires that both environment variables are set.
let host = env::var(SERVICE_HOSTENV).map_err(Error::ReadEnvironmentVariable)?;
let port = env::var(SERVICE_PORTENV)
.map_err(Error::ReadEnvironmentVariable)?
.parse::<u16>()
.map_err(Error::ParseClusterPort)?;

try_uri(&host, port)
}

fn try_uri(host: &str, port: u16) -> Result<http::Uri, Error> {
// Format a host and, if not using 443, a port.
//
// Ensure that IPv6 addresses are properly bracketed.
const HTTPS: &str = "https";
let uri = match host.parse::<std::net::IpAddr>() {
Ok(ip) => {
if port == 443 {
if ip.is_ipv6() {
format!("{HTTPS}://[{ip}]")
} else {
format!("{HTTPS}://{ip}")
}
} else {
let addr = std::net::SocketAddr::new(ip, port);
format!("{HTTPS}://{addr}")
}
}
Err(_) => {
if port == 443 {
format!("{HTTPS}://{host}")
} else {
format!("{HTTPS}://{host}:{port}")
}
}
};
clux marked this conversation as resolved.
Show resolved Hide resolved

uri.parse().map_err(Error::ParseClusterUrl)
olix0r marked this conversation as resolved.
Show resolved Hide resolved
}

pub fn token_file() -> String {
SERVICE_TOKENFILE.to_owned()
}
Expand All @@ -43,3 +101,52 @@ pub fn load_cert() -> Result<Vec<Vec<u8>>, Error> {
pub fn load_default_ns() -> Result<String, Error> {
std::fs::read_to_string(SERVICE_DEFAULT_NS).map_err(Error::ReadDefaultNamespace)
}

#[test]
fn test_kube_name() {
assert_eq!(
try_uri("fake.io", 8080).unwrap().to_string(),
"https://fake.io:8080/"
);
}

#[test]
fn test_kube_name_default_port() {
assert_eq!(try_uri("kubernetes.default.svc", 443).unwrap(), kube_dns())
}

#[test]
fn test_kube_ipv4() {
assert_eq!(
try_uri("10.11.12.13", 6443).unwrap().to_string(),
"https://10.11.12.13:6443/"
);
}

#[test]
fn test_kube_ipv4_default_port() {
assert_eq!(
try_uri("10.11.12.13", 443).unwrap().to_string(),
"https://10.11.12.13/"
);
}

#[test]
fn test_kube_ipv6() {
assert_eq!(
try_uri("2001:0db8:85a3:0000:0000:8a2e:0370:7334", 6443)
.unwrap()
.to_string(),
"https://[2001:db8:85a3::8a2e:370:7334]:6443/"
);
}

#[test]
fn test_kube_ipv6_default_port() {
assert_eq!(
try_uri("2001:0db8:85a3:0000:0000:8a2e:0370:7334", 443)
.unwrap()
.to_string(),
"https://[2001:db8:85a3::8a2e:370:7334]/"
);
}
69 changes: 54 additions & 15 deletions kube-client/src/config/mod.rs
Expand Up @@ -179,14 +179,15 @@ impl Config {
}
}

/// Infer the configuration from the environment
/// Infer a Kubernetes client configuration.
///
/// Done by attempting to load the local kubec-config first, and
/// then if that fails, trying the in-cluster environment variables .
/// First, a user's kubeconfig is loaded from `KUBECONFIG` or
/// `~/.kube/config`. If that fails, an in-cluster config is loaded via
/// [`Config::incluster`]. If inference from both sources fails, then an
/// error is returned.
///
/// Fails if inference from both sources fails
///
/// Applies debug overrides, see [`Config::apply_debug_overrides`] for more details
/// [`Config::apply_debug_overrides`] is used to augment the loaded
/// configuration based on the environment.
pub async fn infer() -> Result<Self, InferConfigError> {
let mut config = match Self::from_kubeconfig(&KubeConfigOptions::default()).await {
Err(kubeconfig_err) => {
Expand All @@ -195,8 +196,8 @@ impl Config {
"no local config found, falling back to local in-cluster config"
);

Self::from_cluster_env().map_err(|in_cluster_err| InferConfigError {
in_cluster: in_cluster_err,
Self::incluster().map_err(|in_cluster| InferConfigError {
in_cluster,
kubeconfig: kubeconfig_err,
})?
}
Expand All @@ -206,13 +207,52 @@ impl Config {
Ok(config)
}

/// Create configuration from the cluster's environment variables
/// Load an in-cluster Kubernetes client configuration using
/// [`Config::incluster_env`].
#[cfg(not(feature = "rustls-tls"))]
pub fn incluster() -> Result<Self, InClusterError> {
Self::incluster_env()
}

/// Load an in-cluster Kubernetes client configuration using
/// [`Config::incluster_dns`].
///
/// The `rustls-tls` feature is currently incompatible with
/// [`Config::incluster_env`]. See
/// <https://github.com/kube-rs/kube-rs/issues/1003>.
#[cfg(feature = "rustls-tls")]
pub fn incluster() -> Result<Self, InClusterError> {
Self::incluster_dns()
}

/// Load an in-cluster config using the `KUBERNETES_SERVICE_HOST` and
/// `KUBERNETES_SERVICE_PORT` environment variables.
///
/// A service account's token must be available in
/// `/var/run/secrets/kubernetes.io/serviceaccount/`.
///
/// This method matches the behavior of the official Kubernetes client
/// libraries, but it is not compatible with the `rustls-tls` feature . When
/// this feature is enabled, [`Config::incluster_dns`] should be used
/// instead. See <https://github.com/kube-rs/kube-rs/issues/1003>.
pub fn incluster_env() -> Result<Self, InClusterError> {
let uri = incluster_config::try_kube_from_env()?;
Self::incluster_with_uri(uri)
}

/// Load an in-cluster config using the API server at
/// `https://kubernetes.default.svc`.
///
/// A service account's token must be available in
/// `/var/run/secrets/kubernetes.io/serviceaccount/`.
///
/// This follows the standard [API Access from a Pod](https://kubernetes.io/docs/tasks/access-application-cluster/access-cluster/#accessing-the-api-from-a-pod)
/// and relies on you having the service account's token mounted,
/// as well as having given the service account rbac access to do what you need.
pub fn from_cluster_env() -> Result<Self, InClusterError> {
let cluster_url = incluster_config::kube_dns();
/// This behavior does not match that of the official Kubernetes clients,
/// but this approach is compatible with the `rustls-tls` feature.
pub fn incluster_dns() -> Result<Self, InClusterError> {
Self::incluster_with_uri(incluster_config::kube_dns())
}

fn incluster_with_uri(cluster_url: http::uri::Uri) -> Result<Self, InClusterError> {
let default_namespace = incluster_config::load_default_ns()?;
let root_cert = incluster_config::load_cert()?;

Expand Down Expand Up @@ -378,7 +418,6 @@ pub use file_config::{
NamedContext, NamedExtension, Preferences,
};


#[cfg(test)]
mod tests {
#[cfg(not(feature = "client"))] // want to ensure this works without client features
Expand Down