Skip to content

Commit

Permalink
Add support for passing cluster information for AuthExec commands.
Browse files Browse the repository at this point in the history
Signed-off-by: Aviram Hassan <aviramyhassan@gmail.com>
  • Loading branch information
aviramha committed Oct 31, 2023
1 parent 17f1cb5 commit 2558b89
Show file tree
Hide file tree
Showing 4 changed files with 122 additions and 7 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 @@ -511,6 +515,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 @@ -551,13 +558,20 @@ fn auth_exec(auth: &ExecConfig) -> Result<ExecCredential, Error> {
cmd.stdin(std::process::Stdio::piped());
}

let mut exec_credential_spec = ExecCredentialSpec {
interactive: Some(interactive),

Check warning on line 562 in kube-client/src/client/auth/mod.rs

View check run for this annotation

Codecov / codecov/patch

kube-client/src/client/auth/mod.rs#L562

Added line #L562 was not covered by tests
cluster: None,
};

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

Check warning on line 567 in kube-client/src/client/auth/mod.rs

View check run for this annotation

Codecov / codecov/patch

kube-client/src/client/auth/mod.rs#L566-L567

Added lines #L566 - L567 were not covered by tests
}

// 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),

Check warning on line 574 in kube-client/src/client/auth/mod.rs

View check run for this annotation

Codecov / codecov/patch

kube-client/src/client/auth/mod.rs#L574

Added line #L574 was not covered by tests
status: None,
})
.map_err(Error::AuthExecSerialize)?;
Expand Down
95 changes: 95 additions & 0 deletions 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

Check warning on line 587 in kube-client/src/config/file_config.rs

View check run for this annotation

Codecov / codecov/patch

kube-client/src/config/file_config.rs#L578-L587

Added lines #L578 - L587 were not covered by tests
.iter()
.find(|extension| extension.name == CLUSTER_EXTENSION_KEY)
.map(|extension| extension.extension.clone())

Check warning on line 590 in kube-client/src/config/file_config.rs

View check run for this annotation

Codecov / codecov/patch

kube-client/src/config/file_config.rs#L589-L590

Added lines #L589 - L590 were not covered by tests
}),
})
}
}

fn load_from_base64_or_file<P: AsRef<Path>>(
value: &Option<&str>,
file: &Option<P>,
Expand Down Expand Up @@ -561,6 +629,33 @@ 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)

Check warning on line 640 in kube-client/src/config/file_config.rs

View check run for this annotation

Codecov / codecov/patch

kube-client/src/config/file_config.rs#L636-L640

Added lines #L636 - L640 were not covered by tests
}
None => <Option<String>>::serialize(&None, s),

Check warning on line 642 in kube-client/src/config/file_config.rs

View check run for this annotation

Codecov / codecov/patch

kube-client/src/config/file_config.rs#L642

Added line #L642 was not covered by tests
}
}

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)?,

Check warning on line 652 in kube-client/src/config/file_config.rs

View check run for this annotation

Codecov / codecov/patch

kube-client/src/config/file_config.rs#L646-L652

Added lines #L646 - L652 were not covered by tests
)),
None => Ok(None),

Check warning on line 654 in kube-client/src/config/file_config.rs

View check run for this annotation

Codecov / codecov/patch

kube-client/src/config/file_config.rs#L654

Added line #L654 was not covered by tests
}
}
}

#[cfg(test)]
mod tests {
use super::*;
Expand Down
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()?);

Check warning on line 95 in kube-client/src/config/file_loader.rs

View check run for this annotation

Codecov / codecov/patch

kube-client/src/config/file_loader.rs#L94-L95

Added lines #L94 - L95 were not covered by tests
}
}

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 2558b89

Please sign in to comment.