Skip to content

Commit

Permalink
Merge pull request #237 from teozkr/feature/structured-errors
Browse files Browse the repository at this point in the history
Structured kubeconfig errors
  • Loading branch information
clux authored May 8, 2020
2 parents 109c3bb + 79eba25 commit 5a69e42
Show file tree
Hide file tree
Showing 8 changed files with 207 additions and 95 deletions.
17 changes: 9 additions & 8 deletions kube/src/config/exec.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
use std::process::Command;

use crate::{config::ExecConfig, Error, Result};
use crate::{config::ExecConfig, error::ConfigError, Result};

use serde::{Deserialize, Serialize};

Expand Down Expand Up @@ -46,15 +46,16 @@ pub fn auth_exec(auth: &ExecConfig) -> Result<ExecCredential> {
});
cmd.envs(envs);
}
let out = cmd
.output()
.map_err(|e| Error::Kubeconfig(format!("Unable to run auth exec: {}", e)))?;
let out = cmd.output().map_err(ConfigError::AuthExecStart)?;
if !out.status.success() {
let err = format!("command `{:?}` failed: {:?}", cmd, out);
return Err(Error::Kubeconfig(err));
return Err(ConfigError::AuthExecRun {
cmd: format!("{:?}", cmd),
status: out.status,
out,
}
.into());
}
let creds = serde_json::from_slice(&out.stdout)
.map_err(|e| Error::Kubeconfig(format!("Unable to parse auth exec result: {}", e)))?;
let creds = serde_json::from_slice(&out.stdout).map_err(ConfigError::AuthExecParse)?;

Ok(creds)
}
14 changes: 7 additions & 7 deletions kube/src/config/file_config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

use std::{collections::HashMap, fs::File, path::Path};

use crate::{config::utils, oauth2, Error, Result};
use crate::{config::utils, error::ConfigError, oauth2, Result};

use serde::{Deserialize, Serialize};

Expand Down Expand Up @@ -136,8 +136,11 @@ pub struct Context {
impl Kubeconfig {
/// Read a Config from an arbitrary location
pub fn read_from<P: AsRef<Path>>(path: P) -> Result<Kubeconfig> {
let f = File::open(path).map_err(|e| Error::Kubeconfig(format!("{}", e)))?;
let config = serde_yaml::from_reader(f).map_err(|e| Error::Kubeconfig(format!("{}", e)))?;
let f = File::open(&path).map_err(|source| ConfigError::ReadFile {
path: path.as_ref().into(),
source,
})?;
let config = serde_yaml::from_reader(f).map_err(ConfigError::ParseYaml)?;
Ok(config)
}

Expand All @@ -154,8 +157,7 @@ impl Cluster {
return Ok(None);
}
let res =
utils::data_or_file_with_base64(&self.certificate_authority_data, &self.certificate_authority)
.map_err(|e| Error::Kubeconfig(format!("{}", e)))?;
utils::data_or_file_with_base64(&self.certificate_authority_data, &self.certificate_authority)?;
Ok(Some(res))
}
}
Expand Down Expand Up @@ -185,11 +187,9 @@ impl AuthInfo {

pub(crate) fn load_client_certificate(&self) -> Result<Vec<u8>> {
utils::data_or_file_with_base64(&self.client_certificate_data, &self.client_certificate)
.map_err(|e| Error::Kubeconfig(format!("{}", e)))
}

pub(crate) fn load_client_key(&self) -> Result<Vec<u8>> {
utils::data_or_file_with_base64(&self.client_key_data, &self.client_key)
.map_err(|e| Error::Kubeconfig(format!("{}", e)))
}
}
21 changes: 13 additions & 8 deletions kube/src/config/file_loader.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
use std::path::Path;

#[cfg(feature = "native-tls")]
use openssl::{pkcs12::Pkcs12, pkey::PKey, x509::X509};
use reqwest::{Certificate, Identity};
Expand All @@ -8,7 +6,7 @@ use super::{
file_config::{AuthInfo, Cluster, Context, Kubeconfig},
utils,
};
use crate::{Error, Result};
use crate::{error::ConfigError, Error, Result};

/// KubeConfigOptions stores options used when loading kubeconfig file.
#[derive(Default, Clone)]
Expand Down Expand Up @@ -45,8 +43,9 @@ pub struct ConfigLoader {
impl ConfigLoader {
/// Returns a config loader based on the cluster information from the kubeconfig file.
pub async fn new_from_options(options: &KubeConfigOptions) -> Result<Self> {
let kubeconfig_path =
utils::find_kubeconfig().map_err(|e| Error::Kubeconfig(format!("Unable to load file: {}", e)))?;
let kubeconfig_path = utils::find_kubeconfig()
.map_err(Box::new)
.map_err(ConfigError::LoadConfigFile)?;

let config = Kubeconfig::read_from(kubeconfig_path)?;
let loader = Self::load(
Expand Down Expand Up @@ -84,14 +83,18 @@ impl ConfigLoader {
.iter()
.find(|named_context| &named_context.name == context_name)
.map(|named_context| &named_context.context)
.ok_or_else(|| Error::Kubeconfig("Unable to load current context".into()))?;
.ok_or_else(|| ConfigError::LoadContext {
context_name: context_name.clone(),
})?;
let cluster_name = cluster.unwrap_or(&current_context.cluster);
let cluster = config
.clusters
.iter()
.find(|named_cluster| &named_cluster.name == cluster_name)
.map(|named_cluster| &named_cluster.cluster)
.ok_or_else(|| Error::Kubeconfig("Unable to load cluster of context".into()))?;
.ok_or_else(|| ConfigError::LoadClusterOfContext {
cluster_name: cluster_name.clone(),
})?;
let user_name = user.unwrap_or(&current_context.user);

let mut user_opt = None;
Expand All @@ -102,7 +105,9 @@ impl ConfigLoader {
user_opt = Some(user);
}
}
let user = user_opt.ok_or_else(|| Error::Kubeconfig("Unable to find named user".into()))?;
let user = user_opt.ok_or_else(|| ConfigError::FindUser {
user_name: user_name.clone(),
})?;
Ok(ConfigLoader {
current_context: current_context.clone(),
cluster: cluster.clone(),
Expand Down
4 changes: 2 additions & 2 deletions kube/src/config/incluster_config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ use std::env;
use crate::{Error, Result};
use reqwest::Certificate;

use crate::config::utils;
use crate::{error::ConfigError, config::utils};

pub const SERVICE_HOSTENV: &str = "KUBERNETES_SERVICE_HOST";
pub const SERVICE_PORTENV: &str = "KUBERNETES_SERVICE_PORT";
Expand Down Expand Up @@ -34,7 +34,7 @@ pub fn load_token() -> Result<String> {
/// Returns certification from specified path in cluster.
pub fn load_cert() -> Result<Certificate> {
let ca = utils::data_or_file_with_base64(&None, &Some(SERVICE_CERTFILE))?;
Certificate::from_pem(&ca).map_err(|e| Error::Kubeconfig(format!("{}", e)))
Certificate::from_pem(&ca).map_err(ConfigError::LoadCert).map_err(Error::from)
}

/// Returns the default namespace from specified path in cluster.
Expand Down
72 changes: 33 additions & 39 deletions kube/src/config/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ mod file_loader;
mod incluster_config;
mod utils;

use crate::{Error, Result};
use crate::{error::ConfigError, Result};
pub use file_loader::KubeConfigOptions;
use file_loader::{ConfigLoader, Der};

Expand All @@ -32,16 +32,12 @@ impl Authentication {
async fn to_header(&self) -> Result<Option<header::HeaderValue>> {
match self {
Self::None => Ok(None),
Self::Basic(value) => {
Ok(Some(header::HeaderValue::from_str(value).map_err(|e| {
Error::Kubeconfig(format!("Invalid basic auth: {}", e))
})?))
}
Self::Token(value) => {
Ok(Some(header::HeaderValue::from_str(value).map_err(|e| {
Error::Kubeconfig(format!("Invalid bearer token: {}", e))
})?))
}
Self::Basic(value) => Ok(Some(
header::HeaderValue::from_str(value).map_err(ConfigError::InvalidBasicAuth)?,
)),
Self::Token(value) => Ok(Some(
header::HeaderValue::from_str(value).map_err(ConfigError::InvalidBearerToken)?,
)),
Self::RefreshableToken(data, loader) => {
let mut locked_data = data.lock().await;
// Add some wiggle room onto the current timestamp so we don't get any race
Expand All @@ -54,14 +50,12 @@ impl Authentication {
locked_data.0 = new_token;
locked_data.1 = new_expire;
} else {
return Err(Error::Kubeconfig(
"Tried to refresh a token and got a non-refreshable token response".to_owned(),
));
return Err(ConfigError::UnrefreshableTokenResponse.into());
}
}
Ok(Some(header::HeaderValue::from_str(&locked_data.0).map_err(
|e| Error::Kubeconfig(format!("Invalid bearer token: {}", e)),
)?))
Ok(Some(
header::HeaderValue::from_str(&locked_data.0).map_err(ConfigError::InvalidBearerToken)?,
))
}
}
}
Expand Down Expand Up @@ -123,12 +117,15 @@ impl Config {
/// Fails if inference from both sources fails
pub async fn infer() -> Result<Self> {
match Self::new_from_cluster_env() {
Err(e1) => {
trace!("No in-cluster config found: {}", e1);
Err(cluster_env_err) => {
trace!("No in-cluster config found: {}", cluster_env_err);
trace!("Falling back to local kubeconfig");
let config = Self::new_from_user_kubeconfig(&KubeConfigOptions::default())
.await
.map_err(|e2| Error::Kubeconfig(format!("Failed to infer config: {}, {}", e1, e2)))?;
.map_err(|kubeconfig_err| ConfigError::ConfigInferenceExhausted {
cluster_env: Box::new(cluster_env_err),
kubeconfig: Box::new(kubeconfig_err),
})?;

Ok(config)
}
Expand All @@ -138,23 +135,22 @@ impl Config {

/// Read the config from the cluster's environment variables
pub fn new_from_cluster_env() -> Result<Self> {
let cluster_url = incluster_config::kube_server().ok_or_else(|| {
Error::Kubeconfig(format!(
"Unable to load in cluster config, {} and {} must be defined",
incluster_config::SERVICE_HOSTENV,
incluster_config::SERVICE_PORTENV
))
})?;
let cluster_url = reqwest::Url::parse(&cluster_url)
.map_err(|e| Error::Kubeconfig(format!("Malformed url: {}", e)))?;
let cluster_url =
incluster_config::kube_server().ok_or_else(|| ConfigError::MissingInClusterVariables {
hostenv: incluster_config::SERVICE_HOSTENV,
portenv: incluster_config::SERVICE_PORTENV,
})?;
let cluster_url = reqwest::Url::parse(&cluster_url)?;

let default_ns = incluster_config::load_default_ns()
.map_err(|e| Error::Kubeconfig(format!("Unable to load incluster default namespace: {}", e)))?;
.map_err(Box::new)
.map_err(ConfigError::InvalidInClusterNamespace)?;

let root_cert = incluster_config::load_cert()?;

let token = incluster_config::load_token()
.map_err(|e| Error::Kubeconfig(format!("Unable to load in cluster token: {}", e)))?;
.map_err(Box::new)
.map_err(ConfigError::InvalidInClusterToken)?;

Ok(Self {
cluster_url,
Expand Down Expand Up @@ -186,8 +182,7 @@ impl Config {
}

fn new_from_loader(loader: ConfigLoader) -> Result<Self> {
let cluster_url = reqwest::Url::parse(&loader.cluster.server)
.map_err(|e| Error::Kubeconfig(format!("Malformed url: {}", e)))?;
let cluster_url = reqwest::Url::parse(&loader.cluster.server)?;

let default_ns = loader
.current_context
Expand Down Expand Up @@ -269,13 +264,12 @@ fn load_auth_header(loader: &ConfigLoader) -> Result<Authentication> {
None => {
if let Some(exec) = &loader.user.exec {
let creds = exec::auth_exec(exec)?;
let status = creds.status.ok_or_else(|| {
Error::Kubeconfig("exec-plugin response did not contain a status".into())
})?;
let status = creds.status.ok_or_else(|| ConfigError::ExecPluginFailed)?;
let expiration = match status.expiration_timestamp {
Some(ts) => Some(ts.parse::<DateTime<Utc>>().map_err(|e| {
Error::Kubeconfig(format!("Malformed expriation date on token: {}", e))
})?),
Some(ts) => Some(
ts.parse::<DateTime<Utc>>()
.map_err(ConfigError::MalformedTokenExpirationDate)?,
),
None => None,
};
(status.token, expiration)
Expand Down
41 changes: 24 additions & 17 deletions kube/src/config/utils.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ use std::{
path::{Path, PathBuf},
};

use crate::{Error, Result};
use crate::{error::ConfigError, Error, Result};
use chrono::{DateTime, Utc};
use dirs::home_dir;

Expand All @@ -13,7 +13,7 @@ const KUBECONFIG: &str = "KUBECONFIG";
pub fn find_kubeconfig() -> Result<PathBuf> {
kubeconfig_path()
.or_else(default_kube_path)
.ok_or_else(|| Error::Kubeconfig("Failed to find path of kubeconfig".into()))
.ok_or_else(|| ConfigError::NoKubeconfigPath.into())
}

/// Returns kubeconfig path from specified environment variable.
Expand All @@ -28,37 +28,44 @@ pub fn default_kube_path() -> Option<PathBuf> {

pub fn data_or_file_with_base64<P: AsRef<Path>>(data: &Option<String>, file: &Option<P>) -> Result<Vec<u8>> {
match (data, file) {
(Some(d), _) => {
base64::decode(&d).map_err(|e| Error::Kubeconfig(format!("Failed to decode base64: {}", e)))
}
(Some(d), _) => base64::decode(&d)
.map_err(ConfigError::Base64Decode)
.map_err(Error::Kubeconfig),
(_, Some(f)) => {
let f = (*f).as_ref();
let abs_file = if f.is_absolute() {
f.to_path_buf()
} else {
find_kubeconfig().and_then(|cfg| {
cfg.parent().map(|kubedir| kubedir.join(f)).ok_or_else(|| {
Error::Kubeconfig(format!("Failed to compute the absolute path of '{:?}'", f))
})
cfg.parent()
.map(|kubedir| kubedir.join(f))
.ok_or_else(|| ConfigError::NoAbsolutePath { path: f.into() }.into())
})?
};
// dbg!(&abs_file);
fs::read(&abs_file)
.map_err(|e| Error::Kubeconfig(format!("Failed to read {:?}: {}", abs_file, e)))
fs::read(&abs_file).map_err(|source| {
ConfigError::ReadFile {
path: abs_file,
source,
}
.into()
})
}
_ => Err(Error::Kubeconfig(
"Failed to get data/file with base64 format".into(),
)),
_ => Err(ConfigError::NoBase64FileOrData.into()),
}
}

pub fn data_or_file<P: AsRef<Path>>(data: &Option<String>, file: &Option<P>) -> Result<String> {
match (data, file) {
(Some(d), _) => Ok(d.to_string()),
(_, Some(f)) => {
fs::read_to_string(f).map_err(|e| Error::Kubeconfig(format!("Failed to read file: {}", e)))
}
_ => Err(Error::Kubeconfig("Failed to get data/file".into())),
(_, Some(f)) => fs::read_to_string(f).map_err(|source| {
ConfigError::ReadFile {
path: f.as_ref().into(),
source,
}
.into()
}),
_ => Err(ConfigError::NoFileOrData.into()),
}
}

Expand Down
Loading

0 comments on commit 5a69e42

Please sign in to comment.