Skip to content

Commit

Permalink
Provide cluster info to exec plugins (#1331)
Browse files Browse the repository at this point in the history
* Add support for passing cluster information for AuthExec commands.

Signed-off-by: Aviram Hassan <[email protected]>

* tests

Signed-off-by: Aviram Hassan <[email protected]>

---------

Signed-off-by: Aviram Hassan <[email protected]>
Co-authored-by: Eirik A <[email protected]>
  • Loading branch information
aviramha and clux authored Nov 23, 2023
1 parent bdda76e commit 4493297
Show file tree
Hide file tree
Showing 4 changed files with 169 additions and 8 deletions.
22 changes: 18 additions & 4 deletions kube-client/src/client/auth/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ use thiserror::Error;
use tokio::sync::{Mutex, RwLock};
use tower::{filter::AsyncPredicate, BoxError};

use crate::config::{AuthInfo, AuthProviderConfig, ExecConfig, ExecInteractiveMode};
use crate::config::{AuthInfo, AuthProviderConfig, ExecAuthCluster, ExecConfig, ExecInteractiveMode};

#[cfg(feature = "oauth")] mod oauth;
#[cfg(feature = "oauth")] pub use oauth::Error as OAuthError;
Expand Down Expand Up @@ -98,6 +98,10 @@ pub enum Error {
#[cfg_attr(docsrs, doc(cfg(feature = "oidc")))]
#[error("failed OIDC: {0}")]
Oidc(#[source] oidc_errors::Error),

/// cluster spec missing while `provideClusterInfo` is true
#[error("Cluster spec must be populated when `provideClusterInfo` is true")]
ExecMissingClusterInfo,
}

#[derive(Debug, Clone)]
Expand Down Expand Up @@ -521,6 +525,9 @@ pub struct ExecCredential {
pub struct ExecCredentialSpec {
#[serde(skip_serializing_if = "Option::is_none")]
interactive: Option<bool>,

#[serde(skip_serializing_if = "Option::is_none")]
cluster: Option<ExecAuthCluster>,
}

/// ExecCredentialStatus holds credentials for the transport to use.
Expand Down Expand Up @@ -561,13 +568,20 @@ fn auth_exec(auth: &ExecConfig) -> Result<ExecCredential, Error> {
cmd.stdin(std::process::Stdio::piped());
}

let mut exec_credential_spec = ExecCredentialSpec {
interactive: Some(interactive),
cluster: None,
};

if auth.provide_cluster_info {
exec_credential_spec.cluster = Some(auth.cluster.clone().ok_or(Error::ExecMissingClusterInfo)?);
}

// Provide exec info to child process
let exec_info = serde_json::to_string(&ExecCredential {
api_version: auth.api_version.clone(),
kind: "ExecCredential".to_string().into(),
spec: Some(ExecCredentialSpec {
interactive: Some(interactive),
}),
spec: Some(exec_credential_spec),
status: None,
})
.map_err(Error::AuthExecSerialize)?;
Expand Down
143 changes: 142 additions & 1 deletion kube-client/src/config/file_config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,9 @@ use serde::{Deserialize, Deserializer, Serialize, Serializer};

use super::{KubeconfigError, LoadDataError};

/// [`CLUSTER_EXTENSION_KEY`] is reserved in the cluster extensions list for exec plugin config.
const CLUSTER_EXTENSION_KEY: &str = "client.authentication.k8s.io/exec";

/// [`Kubeconfig`] represents information on how to connect to a remote Kubernetes cluster
///
/// Stored in `~/.kube/config` by default, but can be distributed across multiple paths in passed through `KUBECONFIG`.
Expand Down Expand Up @@ -278,6 +281,19 @@ pub struct ExecConfig {
#[serde(rename = "interactiveMode")]
#[serde(skip_serializing_if = "Option::is_none")]
pub interactive_mode: Option<ExecInteractiveMode>,

/// ProvideClusterInfo determines whether or not to provide cluster information,
/// which could potentially contain very large CA data, to this exec plugin as a
/// part of the KUBERNETES_EXEC_INFO environment variable. By default, it is set
/// to false. Package k8s.io/client-go/tools/auth/exec provides helper methods for
/// reading this environment variable.
#[serde(default, rename = "provideClusterInfo")]
pub provide_cluster_info: bool,

/// Cluster information to pass to the plugin.
/// Should be used only when `provide_cluster_info` is True.
#[serde(skip)]
pub cluster: Option<ExecAuthCluster>,
}

/// ExecInteractiveMode define the interactity of the child process
Expand Down Expand Up @@ -525,6 +541,58 @@ impl AuthInfo {
}
}

/// Cluster stores information to connect Kubernetes cluster used with auth plugins
/// that have `provideClusterInfo`` enabled.
/// This is a copy of [`kube::config::Cluster`] with certificate_authority passed as bytes without the path.
/// Taken from [clientauthentication/types.go#Cluster](https://github.com/kubernetes/client-go/blob/477cb782cf024bc70b7239f0dca91e5774811950/pkg/apis/clientauthentication/types.go#L73-L129)
#[derive(Clone, Debug, Serialize, Deserialize, Default)]
#[serde(rename_all = "kebab-case")]
#[cfg_attr(test, derive(PartialEq, Eq))]
pub struct ExecAuthCluster {
/// The address of the kubernetes cluster (https://hostname:port).
#[serde(skip_serializing_if = "Option::is_none")]
pub server: Option<String>,
/// Skips the validity check for the server's certificate. This will make your HTTPS connections insecure.
#[serde(skip_serializing_if = "Option::is_none")]
pub insecure_skip_tls_verify: Option<bool>,
/// PEM-encoded certificate authority certificates. Overrides `certificate_authority`
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(with = "base64serde")]
pub certificate_authority_data: Option<Vec<u8>>,
/// URL to the proxy to be used for all requests.
#[serde(skip_serializing_if = "Option::is_none")]
pub proxy_url: Option<String>,
/// Name used to check server certificate.
///
/// If `tls_server_name` is `None`, the hostname used to contact the server is used.
#[serde(skip_serializing_if = "Option::is_none")]
pub tls_server_name: Option<String>,
/// This can be anything
#[serde(skip_serializing_if = "Option::is_none")]
pub config: Option<serde_json::Value>,
}

impl TryFrom<&Cluster> for ExecAuthCluster {
type Error = KubeconfigError;

fn try_from(cluster: &crate::config::Cluster) -> Result<Self, KubeconfigError> {
let certificate_authority_data = cluster.load_certificate_authority()?;
Ok(Self {
server: cluster.server.clone(),
insecure_skip_tls_verify: cluster.insecure_skip_tls_verify,
certificate_authority_data,
proxy_url: cluster.proxy_url.clone(),
tls_server_name: cluster.tls_server_name.clone(),
config: cluster.extensions.as_ref().and_then(|extensions| {
extensions
.iter()
.find(|extension| extension.name == CLUSTER_EXTENSION_KEY)
.map(|extension| extension.extension.clone())
}),
})
}
}

fn load_from_base64_or_file<P: AsRef<Path>>(
value: &Option<&str>,
file: &Option<P>,
Expand Down Expand Up @@ -561,10 +629,39 @@ fn default_kube_path() -> Option<PathBuf> {
home::home_dir().map(|h| h.join(".kube").join("config"))
}

mod base64serde {
use base64::Engine;
use serde::{Deserialize, Deserializer, Serialize, Serializer};

pub fn serialize<S: Serializer>(v: &Option<Vec<u8>>, s: S) -> Result<S::Ok, S::Error> {
match v {
Some(v) => {
let encoded = base64::engine::general_purpose::STANDARD.encode(v);
String::serialize(&encoded, s)
}
None => <Option<String>>::serialize(&None, s),
}
}

pub fn deserialize<'de, D: Deserializer<'de>>(d: D) -> Result<Option<Vec<u8>>, D::Error> {
let data = <Option<String>>::deserialize(d)?;
match data {
Some(data) => Ok(Some(
base64::engine::general_purpose::STANDARD
.decode(data.as_bytes())
.map_err(serde::de::Error::custom)?,
)),
None => Ok(None),
}
}
}

#[cfg(test)]
mod tests {
use crate::config::file_loader::ConfigLoader;

use super::*;
use serde_json::Value;
use serde_json::{json, Value};
use std::str::FromStr;

#[test]
Expand Down Expand Up @@ -822,4 +919,48 @@ password: kube_rs

assert_eq!(authinfo_debug_output, expected_output)
}

#[tokio::test]
async fn authinfo_exec_provide_cluster_info() {
let config = r#"
apiVersion: v1
clusters:
- cluster:
server: https://localhost:8080
extensions:
- name: client.authentication.k8s.io/exec
extension:
audience: foo
other: bar
name: foo-cluster
contexts:
- context:
cluster: foo-cluster
user: foo-user
namespace: bar
name: foo-context
current-context: foo-context
kind: Config
users:
- name: foo-user
user:
exec:
apiVersion: client.authentication.k8s.io/v1alpha1
args:
- arg-1
- arg-2
command: foo-command
provideClusterInfo: true
"#;
let kube_config = Kubeconfig::from_yaml(config).unwrap();
let config_loader = ConfigLoader::load(kube_config, None, None, None).await.unwrap();
let auth_info = config_loader.user;
let exec = auth_info.exec.unwrap();
assert!(exec.provide_cluster_info);
let cluster = exec.cluster.unwrap();
assert_eq!(
cluster.config.unwrap(),
json!({"audience": "foo", "other": "bar"})
);
}
}
8 changes: 7 additions & 1 deletion kube-client/src/config/file_loader.rs
Original file line number Diff line number Diff line change
Expand Up @@ -83,13 +83,19 @@ impl ConfigLoader {
.ok_or_else(|| KubeconfigError::LoadClusterOfContext(cluster_name.clone()))?;

let user_name = user.unwrap_or(&current_context.user);
let user = config
let mut user = config
.auth_infos
.iter()
.find(|named_user| &named_user.name == user_name)
.and_then(|named_user| named_user.auth_info.clone())
.ok_or_else(|| KubeconfigError::FindUser(user_name.clone()))?;

if let Some(exec_config) = &mut user.exec {
if exec_config.provide_cluster_info {
exec_config.cluster = Some((&cluster).try_into()?);
}
}

Ok(ConfigLoader {
current_context,
cluster,
Expand Down
4 changes: 2 additions & 2 deletions kube-client/src/config/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -380,8 +380,8 @@ const DEFAULT_WRITE_TIMEOUT: Duration = Duration::from_secs(295);

// Expose raw config structs
pub use file_config::{
AuthInfo, AuthProviderConfig, Cluster, Context, ExecConfig, ExecInteractiveMode, Kubeconfig,
NamedAuthInfo, NamedCluster, NamedContext, NamedExtension, Preferences,
AuthInfo, AuthProviderConfig, Cluster, Context, ExecAuthCluster, ExecConfig, ExecInteractiveMode,
Kubeconfig, NamedAuthInfo, NamedCluster, NamedContext, NamedExtension, Preferences,
};

#[cfg(test)]
Expand Down

0 comments on commit 4493297

Please sign in to comment.