From 44932971fe12ddeebde4a85bb92b8a2fb7f0e5e3 Mon Sep 17 00:00:00 2001 From: Aviram Hassan Date: Thu, 23 Nov 2023 15:35:21 +0200 Subject: [PATCH] Provide cluster info to exec plugins (#1331) * Add support for passing cluster information for AuthExec commands. Signed-off-by: Aviram Hassan * tests Signed-off-by: Aviram Hassan --------- Signed-off-by: Aviram Hassan Co-authored-by: Eirik A --- kube-client/src/client/auth/mod.rs | 22 +++- kube-client/src/config/file_config.rs | 143 +++++++++++++++++++++++++- kube-client/src/config/file_loader.rs | 8 +- kube-client/src/config/mod.rs | 4 +- 4 files changed, 169 insertions(+), 8 deletions(-) diff --git a/kube-client/src/client/auth/mod.rs b/kube-client/src/client/auth/mod.rs index 74515a483..eaca2d8ea 100644 --- a/kube-client/src/client/auth/mod.rs +++ b/kube-client/src/client/auth/mod.rs @@ -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; @@ -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)] @@ -521,6 +525,9 @@ pub struct ExecCredential { pub struct ExecCredentialSpec { #[serde(skip_serializing_if = "Option::is_none")] interactive: Option, + + #[serde(skip_serializing_if = "Option::is_none")] + cluster: Option, } /// ExecCredentialStatus holds credentials for the transport to use. @@ -561,13 +568,20 @@ fn auth_exec(auth: &ExecConfig) -> Result { 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)?; diff --git a/kube-client/src/config/file_config.rs b/kube-client/src/config/file_config.rs index 4e658145e..1f051f0ca 100644 --- a/kube-client/src/config/file_config.rs +++ b/kube-client/src/config/file_config.rs @@ -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`. @@ -278,6 +281,19 @@ pub struct ExecConfig { #[serde(rename = "interactiveMode")] #[serde(skip_serializing_if = "Option::is_none")] pub interactive_mode: Option, + + /// 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, } /// ExecInteractiveMode define the interactity of the child process @@ -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, + /// 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, + /// PEM-encoded certificate authority certificates. Overrides `certificate_authority` + #[serde(skip_serializing_if = "Option::is_none")] + #[serde(with = "base64serde")] + pub certificate_authority_data: Option>, + /// URL to the proxy to be used for all requests. + #[serde(skip_serializing_if = "Option::is_none")] + pub proxy_url: Option, + /// 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, + /// This can be anything + #[serde(skip_serializing_if = "Option::is_none")] + pub config: Option, +} + +impl TryFrom<&Cluster> for ExecAuthCluster { + type Error = KubeconfigError; + + fn try_from(cluster: &crate::config::Cluster) -> Result { + 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>( value: &Option<&str>, file: &Option

, @@ -561,10 +629,39 @@ fn default_kube_path() -> Option { home::home_dir().map(|h| h.join(".kube").join("config")) } +mod base64serde { + use base64::Engine; + use serde::{Deserialize, Deserializer, Serialize, Serializer}; + + pub fn serialize(v: &Option>, s: S) -> Result { + match v { + Some(v) => { + let encoded = base64::engine::general_purpose::STANDARD.encode(v); + String::serialize(&encoded, s) + } + None => >::serialize(&None, s), + } + } + + pub fn deserialize<'de, D: Deserializer<'de>>(d: D) -> Result>, D::Error> { + let data = >::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] @@ -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"}) + ); + } } diff --git a/kube-client/src/config/file_loader.rs b/kube-client/src/config/file_loader.rs index 2eeb1b01f..152793b03 100644 --- a/kube-client/src/config/file_loader.rs +++ b/kube-client/src/config/file_loader.rs @@ -83,13 +83,19 @@ impl ConfigLoader { .ok_or_else(|| KubeconfigError::LoadClusterOfContext(cluster_name.clone()))?; let user_name = user.unwrap_or(¤t_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, diff --git a/kube-client/src/config/mod.rs b/kube-client/src/config/mod.rs index c114a9f06..3d9e2be77 100644 --- a/kube-client/src/config/mod.rs +++ b/kube-client/src/config/mod.rs @@ -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)]