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

cherry-pick and merge oauth2 progress from ynqa #20

Merged
merged 13 commits into from
Jul 22, 2019
1 change: 1 addition & 0 deletions .travis.yml
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
language: rust
rust:
- stable
cache: cargo
script:
- cargo build
- cargo test -v --all-features
Expand Down
2 changes: 2 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ edition = "2018"

[dependencies]
base64 = "0.9.3"
chrono = "0.4.6"
dirs = "1.0.4"
failure = "0.1.2"
reqwest = "0.9.17"
Expand All @@ -26,6 +27,7 @@ openssl = "0.10.12"
http = "0.1.17"
url = "1.7.2"
log = "0.4.6"
time = "0.1.42"
k8s-openapi = { version = "0.4.0", optional = true }
either = "1.5.2"

Expand Down
2 changes: 1 addition & 1 deletion examples/crd_api.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ use either::Either::{Left, Right};
use serde_json::json;

use kube::{
api::{RawApi, PostParams, DeleteParams, ListParams, Object, ObjectList, Void, PatchParams},
api::{RawApi, PostParams, DeleteParams, ListParams, Object, ObjectList, PatchParams, Void},
client::APIClient,
config,
};
Expand Down
71 changes: 58 additions & 13 deletions src/config/apis.rs
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
use std::collections::HashMap;
use std::fs::File;
use std::path::Path;

use failure::Error;
use serde_yaml;

use failure::ResultExt;
use crate::{Result, ErrorKind};
use crate::config::utils;
use crate::oauth2;

/// Config stores information to connect remote kubernetes cluster.
#[derive(Clone, Debug, Serialize, Deserialize)]
Expand Down Expand Up @@ -87,6 +88,28 @@ pub struct AuthInfo {
pub impersonate: Option<String>,
#[serde(rename = "as-groups")]
pub impersonate_groups: Option<Vec<String>>,

#[serde(rename = "auth-provider")]
pub auth_provider: Option<AuthProviderConfig>,

pub exec: Option<ExecConfig>,
}

/// AuthProviderConfig stores auth for specified cloud provider.
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct AuthProviderConfig {
pub name: String,
pub config: HashMap<String, String>,
}

/// ExecConfig stores credential-plugin configuration.
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct ExecConfig {
#[serde(rename = "apiVersion")]
pub api_version: Option<String>,
pub args: Option<Vec<String>>,
pub command: String,
pub env: Option<Vec<HashMap<String, String>>>,
}

/// NamedContext associates name with context.
Expand All @@ -106,28 +129,50 @@ pub struct Context {
}

impl Config {
pub fn load_config<P: AsRef<Path>>(path: P) -> Result<Config, Error> {
let f = File::open(path)?;
let config = serde_yaml::from_reader(f)?;
pub fn load_config<P: AsRef<Path>>(path: P) -> Result<Config> {
let f = File::open(path)
.context(ErrorKind::KubeConfig("Unable to open config file".into()))?;
let config = serde_yaml::from_reader(f)
.context(ErrorKind::KubeConfig("Unable to parse config file as yaml".into()))?;
Ok(config)
}
}

impl Cluster {
pub fn load_certificate_authority(&self) -> Result<Vec<u8>, Error> {
utils::data_or_file_with_base64(
pub fn load_certificate_authority(&self) -> Result<Vec<u8>> {
let res = utils::data_or_file_with_base64(
&self.certificate_authority_data,
&self.certificate_authority,
)
).context(ErrorKind::KubeConfig("Unable to decode base64 certificates".into()))?;
Ok(res)
}
}

impl AuthInfo {
pub fn load_client_certificate(&self) -> Result<Vec<u8>, Error> {
utils::data_or_file_with_base64(&self.client_certificate_data, &self.client_certificate)
pub fn load_gcp(&mut self) -> Result<bool> {
match &self.auth_provider {
Some(provider) => {
self.token = Some(provider.config["access-token"].clone());
if utils::is_expired(&provider.config["expiry"]) {
let client = oauth2::CredentialsClient::new()?;
let token = client.request_token(&vec![
"https://www.googleapis.com/auth/cloud-platform".to_string(),
])?;
self.token = Some(token.access_token);
}
}
None => {}
};
Ok(true)
}

pub fn load_client_certificate(&self) -> Result<Vec<u8>> {
Ok(utils::data_or_file_with_base64(&self.client_certificate_data, &self.client_certificate)
.context(ErrorKind::KubeConfig("Unable to decode base64 client cert".into()))?)
}

pub fn load_client_key(&self) -> Result<Vec<u8>, Error> {
utils::data_or_file_with_base64(&self.client_key_data, &self.client_key)
pub fn load_client_key(&self) -> Result<Vec<u8>> {
Ok(utils::data_or_file_with_base64(&self.client_key_data, &self.client_key)
.context(ErrorKind::KubeConfig("Unable to decode base64 client key".into()))?)
}
}
59 changes: 59 additions & 0 deletions src/config/exec.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
use std::process::Command;

use failure::ResultExt;
use crate::{Error, Result, ErrorKind};
use crate::config::apis;

/// ExecCredentials is used by exec-based plugins to communicate credentials to
/// HTTP transports.
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct ExecCredential {
pub kind: Option<String>,
#[serde(rename = "apiVersion")]
pub api_version: Option<String>,
pub spec: Option<ExecCredentialSpec>,
pub status: Option<ExecCredentialStatus>,
}

/// ExecCredenitalSpec holds request and runtime specific information provided
/// by transport.
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct ExecCredentialSpec {}

/// ExecCredentialStatus holds credentials for the transport to use.
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct ExecCredentialStatus {
#[serde(rename = "expirationTimestamp")]
pub expiration_timestamp: Option<chrono::DateTime<chrono::Utc>>,
pub token: Option<String>,
#[serde(rename = "clientCertificateData")]
pub client_certificate_data: Option<String>,
#[serde(rename = "clientKeyData")]
pub client_key_data: Option<String>,
}

pub fn auth_exec(auth: &apis::ExecConfig) -> Result<ExecCredential> {
let mut cmd = Command::new(&auth.command);
if let Some(args) = &auth.args {
cmd.args(args);
}
if let Some(env) = &auth.env {
let envs = env
.iter()
.flat_map(|env| match (env.get("name"), env.get("value")) {
(Some(name), Some(value)) => Some((name, value)),
_ => None,
});
cmd.envs(envs);
}
let out = cmd.output()
.context(ErrorKind::KubeConfig("Unable to run auth exec".into()))?;
if !out.status.success() {
let err = format!("command `{:?}` failed: {:?}", cmd, out);
return Err(Error::from(ErrorKind::KubeConfig(err)));
}
let creds = serde_json::from_slice(&out.stdout)
.context(ErrorKind::KubeConfig("Unable to parse auth exec result as json".into()))?;

Ok(creds)
}
59 changes: 37 additions & 22 deletions src/config/kube_config.rs
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
use std::path::Path;

use failure::Error;
use openssl::pkcs12::Pkcs12;
use openssl::pkey::PKey;
use openssl::x509::X509;

use openssl::{
pkcs12::Pkcs12,
pkey::PKey,
x509::X509,
};
use failure::ResultExt;
use crate::{Result, Error, ErrorKind};
use crate::config::apis::{AuthInfo, Cluster, Config, Context};

/// KubeConfigLoader loads current context, cluster, and authentication information.
Expand All @@ -16,47 +17,61 @@ pub struct KubeConfigLoader {
}

impl KubeConfigLoader {
pub fn load<P: AsRef<Path>>(path: P) -> Result<KubeConfigLoader, Error> {
pub fn load<P: AsRef<Path>>(
path: P,
context: Option<String>,
cluster: Option<String>,
user: Option<String>,
) -> Result<KubeConfigLoader> {
let config = Config::load_config(path)?;
let context_name = context.as_ref().unwrap_or(&config.current_context);
let current_context = config
.contexts
.iter()
.find(|named_context| named_context.name == config.current_context)
.find(|named_context| &named_context.name == context_name)
.map(|named_context| &named_context.context)
.ok_or(format_err!("Unable to load current context"))?;
.ok_or_else(|| ErrorKind::KubeConfig("Unable to load current context".into()))?;
let cluster_name = cluster.as_ref().unwrap_or(&current_context.cluster);
let cluster = config
.clusters
.iter()
.find(|named_cluster| named_cluster.name == current_context.cluster)
.find(|named_cluster| &named_cluster.name == cluster_name)
.map(|named_cluster| &named_cluster.cluster)
.ok_or(format_err!("Unable to load cluster of current context"))?;
.ok_or_else(|| ErrorKind::KubeConfig("Unable to load cluster of context".into()))?;
let user_name = user.as_ref().unwrap_or(&current_context.user);
let user = config
.auth_infos
.iter()
.find(|named_user| named_user.name == current_context.user)
.map(|named_user| &named_user.auth_info)
.ok_or(format_err!("Unable to load user of current context"))?;
.find(|named_user| &named_user.name == user_name)
.map(|named_user| {
let mut user = named_user.auth_info.clone();
match user.load_gcp() {
Ok(_) => Ok(user),
Err(e) => Err(e),
}
})
.ok_or_else(|| ErrorKind::KubeConfig("Unable to load user of context".into()))??;
Ok(KubeConfigLoader {
current_context: current_context.clone(),
cluster: cluster.clone(),
user: user.clone(),
})
}

pub fn p12(&self, password: &str) -> Result<Pkcs12, Error> {
pub fn p12(&self, password: &str) -> Result<Pkcs12> {
let client_cert = &self.user.load_client_certificate()?;
let client_key = &self.user.load_client_key()?;

let x509 = X509::from_pem(&client_cert)?;
let pkey = PKey::private_key_from_pem(&client_key)?;
let x509 = X509::from_pem(&client_cert).context(ErrorKind::SslError)?;
let pkey = PKey::private_key_from_pem(&client_key).context(ErrorKind::SslError)?;

Pkcs12::builder()
Ok(Pkcs12::builder()
.build(password, "kubeconfig", &pkey, &x509)
.map_err(Error::from)
.context(ErrorKind::SslError)?)
}

pub fn ca(&self) -> Result<X509, Error> {
let ca = &self.cluster.load_certificate_authority()?;
X509::from_pem(&ca).map_err(Error::from)
pub fn ca(&self) -> Option<Result<X509>> {
let ca = self.cluster.load_certificate_authority().ok()?;
Some(X509::from_pem(&ca).map_err(|_| Error::from(ErrorKind::SslError)))
}
}
Loading