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

Provide cluster info to exec plugins #1331

Merged
merged 4 commits into from
Nov 23, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 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 @@
#[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 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 @@
cmd.stdin(std::process::Stdio::piped());
}

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

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

View check run for this annotation

Codecov / codecov/patch

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

Added line #L572 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 577 in kube-client/src/client/auth/mod.rs

View check run for this annotation

Codecov / codecov/patch

kube-client/src/client/auth/mod.rs#L576-L577

Added lines #L576 - L577 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 584 in kube-client/src/client/auth/mod.rs

View check run for this annotation

Codecov / codecov/patch

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

Added line #L584 was not covered by tests
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 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 @@
#[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 @@
}
}

/// 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.
aviramha marked this conversation as resolved.
Show resolved Hide resolved
/// 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>,
clux marked this conversation as resolved.
Show resolved Hide resolved
/// 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 @@
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 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 @@

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
Loading