Skip to content

Commit

Permalink
add valid_until to Client
Browse files Browse the repository at this point in the history
Signed-off-by: goenning <[email protected]>
  • Loading branch information
goenning committed Jan 2, 2025
1 parent 6a980c6 commit 6c15783
Show file tree
Hide file tree
Showing 9 changed files with 96 additions and 70 deletions.
4 changes: 2 additions & 2 deletions examples/custom_client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,13 +12,13 @@ async fn main() -> anyhow::Result<()> {

let config = Config::infer().await?;

let https = config.rustls_https_connector()?;
let https = config.rustls_https_connector(None)?;
let service = tower::ServiceBuilder::new()
.layer(config.base_uri_layer())
.option_layer(config.auth_layer()?)
.map_err(BoxError::from)
.service(hyper_util::client::legacy::Client::builder(TokioExecutor::new()).build(https));
let client = Client::new(service, config.default_namespace);
let client = Client::new(service, config.default_namespace, None);

let pods: Api<Pod> = Api::default_namespaced(client);
for p in pods.list(&Default::default()).await? {
Expand Down
4 changes: 2 additions & 2 deletions examples/custom_client_trace.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ async fn main() -> anyhow::Result<()> {
tracing_subscriber::fmt::init();

let config = Config::infer().await?;
let https = config.rustls_https_connector()?;
let https = config.rustls_https_connector(None)?;
let service = ServiceBuilder::new()
.layer(config.base_uri_layer())
// showcase rate limiting; max 10rps, and 4 concurrent
Expand Down Expand Up @@ -57,7 +57,7 @@ async fn main() -> anyhow::Result<()> {
.map_err(BoxError::from)
.service(hyper_util::client::legacy::Client::builder(TokioExecutor::new()).build(https));

let client = Client::new(service, config.default_namespace);
let client = Client::new(service, config.default_namespace, None);

let pods: Api<Pod> = Api::default_namespaced(client);
for p in pods.list(&Default::default()).await? {
Expand Down
2 changes: 1 addition & 1 deletion kube-client/src/api/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -256,7 +256,7 @@ mod test {
#[tokio::test]
async fn scopes_should_allow_correct_interface() {
let (mock_service, _handle) = mock::pair::<Request<Body>, Response<Body>>();
let client = Client::new(mock_service, "default");
let client = Client::new(mock_service, "default", None);

let _: Api<corev1::Node> = Api::all(client.clone());
let _: Api<corev1::Pod> = Api::default_namespaced(client.clone());
Expand Down
19 changes: 12 additions & 7 deletions kube-client/src/client/auth/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -115,7 +115,7 @@ pub(crate) enum Auth {
Basic(String, SecretString),
Bearer(SecretString),
RefreshableToken(RefreshableToken),
Certificate(String, SecretString),
Certificate(String, SecretString, Option<DateTime<Utc>>),
}

// Token file reference. Reloads at least once per minute.
Expand Down Expand Up @@ -227,7 +227,7 @@ impl RefreshableToken {
if Utc::now() + SIXTY_SEC >= locked_data.1 {
// TODO Improve refreshing exec to avoid `Auth::try_from`
match Auth::try_from(&locked_data.2)? {
Auth::None | Auth::Basic(_, _) | Auth::Bearer(_) | Auth::Certificate(_, _) => {
Auth::None | Auth::Basic(_, _) | Auth::Bearer(_) | Auth::Certificate(_, _, _) => {
return Err(Error::UnrefreshableTokenResponse);
}

Expand Down Expand Up @@ -350,16 +350,21 @@ impl TryFrom<&AuthInfo> for Auth {
if let Some(exec) = &auth_info.exec {
let creds = auth_exec(exec)?;
let status = creds.status.ok_or(Error::ExecPluginFailed)?;
if let (Some(client_certificate_data), Some(client_key_data)) =
(status.client_certificate_data, status.client_key_data)
{
return Ok(Self::Certificate(client_certificate_data, client_key_data.into()));
}
let expiration = status
.expiration_timestamp
.map(|ts| ts.parse())
.transpose()
.map_err(Error::MalformedTokenExpirationDate)?;

if let (Some(client_certificate_data), Some(client_key_data)) =
(status.client_certificate_data, status.client_key_data)
{
return Ok(Self::Certificate(
client_certificate_data,
client_key_data.into(),
expiration,
));
}
match (status.token.map(SecretString::from), expiration) {
(Some(token), Some(expire)) => Ok(Self::RefreshableToken(RefreshableToken::Exec(Arc::new(
Mutex::new((token, expire, auth_info.clone())),
Expand Down
17 changes: 13 additions & 4 deletions kube-client/src/client/builder.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
use bytes::Bytes;
use chrono::{DateTime, Utc};
use http::{header::HeaderMap, Request, Response};
use hyper::{
body::Incoming,
Expand Down Expand Up @@ -30,20 +31,22 @@ pub type DynBody = dyn http_body::Body<Data = Bytes, Error = BoxError> + Send +
pub struct ClientBuilder<Svc> {
service: Svc,
default_ns: String,
valid_until: Option<DateTime<Utc>>,
}

impl<Svc> ClientBuilder<Svc> {
/// Construct a [`ClientBuilder`] from scratch with a fully custom [`Service`] stack.
///
/// This method is only intended for advanced use cases, most users will want to use [`ClientBuilder::try_from`] instead,
/// which provides a default stack as a starting point.
pub fn new(service: Svc, default_namespace: impl Into<String>) -> Self
pub fn new(service: Svc, default_namespace: impl Into<String>, valid_until: Option<DateTime<Utc>>) -> Self
where
Svc: Service<Request<Body>>,
{
Self {
service,
default_ns: default_namespace.into(),
valid_until,
}
}

Expand All @@ -52,10 +55,12 @@ impl<Svc> ClientBuilder<Svc> {
let Self {
service: stack,
default_ns,
valid_until,
} = self;
ClientBuilder {
service: layer.layer(stack),
default_ns,
valid_until,
}
}

Expand All @@ -68,7 +73,7 @@ impl<Svc> ClientBuilder<Svc> {
B: http_body::Body<Data = bytes::Bytes> + Send + 'static,
B::Error: Into<BoxError>,
{
Client::new(self.service, self.default_ns)
Client::new(self.service, self.default_ns, self.valid_until)
}
}

Expand Down Expand Up @@ -148,16 +153,19 @@ where
let default_ns = config.default_namespace.clone();
let auth_layer = config.auth_layer()?;

let (exec_identity, expiration) = config.exec_identity_pem();
let identity = exec_identity.or_else(|| config.identity_pem());

let client: hyper_util::client::legacy::Client<_, Body> = {
// Current TLS feature precedence when more than one are set:
// 1. rustls-tls
// 2. openssl-tls
// Create a custom client to use something else.
// If TLS features are not enabled, http connector will be used.
#[cfg(feature = "rustls-tls")]
let connector = config.rustls_https_connector_with_connector(connector)?;
let connector = config.rustls_https_connector_with_connector(connector, identity)?;
#[cfg(all(not(feature = "rustls-tls"), feature = "openssl-tls"))]
let connector = config.openssl_https_connector_with_connector(connector)?;
let connector = config.openssl_https_connector_with_connector(connector, identity)?;
#[cfg(all(not(feature = "rustls-tls"), not(feature = "openssl-tls")))]
if config.cluster_url.scheme() == Some(&http::uri::Scheme::HTTPS) {
// no tls stack situation only works with http scheme
Expand Down Expand Up @@ -250,6 +258,7 @@ where
.layer(service),
),
default_ns,
expiration,
))
}

Expand Down
97 changes: 49 additions & 48 deletions kube-client/src/client/config_ext.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
use std::sync::Arc;

use chrono::{DateTime, Utc};
use http::{header::HeaderName, HeaderValue};
#[cfg(feature = "openssl-tls")] use hyper::rt::{Read, Write};
use hyper_util::client::legacy::connect::HttpConnector;
Expand Down Expand Up @@ -44,7 +45,10 @@ pub trait ConfigExt: private::Sealed {
/// ```
#[cfg_attr(docsrs, doc(cfg(feature = "rustls-tls")))]
#[cfg(feature = "rustls-tls")]
fn rustls_https_connector(&self) -> Result<hyper_rustls::HttpsConnector<HttpConnector>>;
fn rustls_https_connector(
&self,
identity: Option<Vec<u8>>,
) -> Result<hyper_rustls::HttpsConnector<HttpConnector>>;

/// Create [`hyper_rustls::HttpsConnector`] based on config and `connector`.
///
Expand All @@ -67,29 +71,9 @@ pub trait ConfigExt: private::Sealed {
fn rustls_https_connector_with_connector<H>(
&self,
connector: H,
identity: Option<Vec<u8>>,
) -> Result<hyper_rustls::HttpsConnector<H>>;

/// Create [`rustls::ClientConfig`] based on config.
/// # Example
///
/// ```rust
/// # async fn doc() -> Result<(), Box<dyn std::error::Error>> {
/// # use hyper_util::client::legacy::connect::HttpConnector;
/// # use kube::{client::ConfigExt, Config};
/// let config = Config::infer().await?;
/// let https = {
/// let rustls_config = std::sync::Arc::new(config.rustls_client_config()?);
/// let mut http = HttpConnector::new();
/// http.enforce_http(false);
/// hyper_rustls::HttpsConnector::from((http, rustls_config))
/// };
/// # Ok(())
/// # }
/// ```
#[cfg_attr(docsrs, doc(cfg(feature = "rustls-tls")))]
#[cfg(feature = "rustls-tls")]
fn rustls_client_config(&self) -> Result<rustls::ClientConfig>;

/// Create [`hyper_openssl::HttpsConnector`] based on config.
/// # Example
///
Expand All @@ -103,8 +87,10 @@ pub trait ConfigExt: private::Sealed {
/// ```
#[cfg_attr(docsrs, doc(cfg(feature = "openssl-tls")))]
#[cfg(feature = "openssl-tls")]
fn openssl_https_connector(&self)
-> Result<hyper_openssl::client::legacy::HttpsConnector<HttpConnector>>;
fn openssl_https_connector(
&self,
identity: Option<Vec<u8>>,
) -> Result<hyper_openssl::client::legacy::HttpsConnector<HttpConnector>>;

/// Create [`hyper_openssl::HttpsConnector`] based on config and `connector`.
/// # Example
Expand All @@ -125,6 +111,7 @@ pub trait ConfigExt: private::Sealed {
fn openssl_https_connector_with_connector<H>(
&self,
connector: H,
identity: Option<Vec<u8>>,
) -> Result<hyper_openssl::client::legacy::HttpsConnector<H>>
where
H: tower::Service<http::Uri> + Send,
Expand All @@ -151,7 +138,10 @@ pub trait ConfigExt: private::Sealed {
/// ```
#[cfg_attr(docsrs, doc(cfg(feature = "openssl-tls")))]
#[cfg(feature = "openssl-tls")]
fn openssl_ssl_connector_builder(&self) -> Result<openssl::ssl::SslConnectorBuilder>;
fn openssl_ssl_connector_builder(
&self,
identity: Option<Vec<u8>>,
) -> Result<openssl::ssl::SslConnectorBuilder>;
}

mod private {
Expand All @@ -176,7 +166,7 @@ impl ConfigExt for Config {
Auth::RefreshableToken(refreshable) => {
Some(AuthLayer(Either::Right(AsyncFilterLayer::new(refreshable))))
}
Auth::Certificate(_client_certificate_data, _client_key_data) => None,
Auth::Certificate(_client_certificate_data, _client_key_data, _) => None,
})
}

Expand Down Expand Up @@ -206,33 +196,32 @@ impl ConfigExt for Config {
}

#[cfg(feature = "rustls-tls")]
fn rustls_client_config(&self) -> Result<rustls::ClientConfig> {
let identity = self.exec_identity_pem().or_else(|| self.identity_pem());
tls::rustls_tls::rustls_client_config(
identity.as_deref(),
self.root_cert.as_deref(),
self.accept_invalid_certs,
)
.map_err(Error::RustlsTls)
}

#[cfg(feature = "rustls-tls")]
fn rustls_https_connector(&self) -> Result<hyper_rustls::HttpsConnector<HttpConnector>> {
fn rustls_https_connector(
&self,
identity: Option<Vec<u8>>,
) -> Result<hyper_rustls::HttpsConnector<HttpConnector>> {
let mut connector = HttpConnector::new();
connector.enforce_http(false);
self.rustls_https_connector_with_connector(connector)
self.rustls_https_connector_with_connector(connector, identity)
}

#[cfg(feature = "rustls-tls")]
fn rustls_https_connector_with_connector<H>(
&self,
connector: H,
identity: Option<Vec<u8>>,
) -> Result<hyper_rustls::HttpsConnector<H>> {
use hyper_rustls::FixedServerNameResolver;

use crate::client::tls::rustls_tls;

let rustls_config = self.rustls_client_config()?;
let rustls_config = tls::rustls_tls::rustls_client_config(
identity.as_deref(),
self.root_cert.as_deref(),
self.accept_invalid_certs,
)
.map_err(Error::RustlsTls)?;

let mut builder = hyper_rustls::HttpsConnectorBuilder::new()
.with_tls_config(rustls_config)
.https_or_http();
Expand All @@ -248,8 +237,10 @@ impl ConfigExt for Config {
}

#[cfg(feature = "openssl-tls")]
fn openssl_ssl_connector_builder(&self) -> Result<openssl::ssl::SslConnectorBuilder> {
let identity = self.exec_identity_pem().or_else(|| self.identity_pem());
fn openssl_ssl_connector_builder(
&self,
identity: Option<Vec<u8>>,
) -> Result<openssl::ssl::SslConnectorBuilder> {
// TODO: pass self.tls_server_name for openssl
tls::openssl_tls::ssl_connector_builder(identity.as_ref(), self.root_cert.as_ref())
.map_err(|e| Error::OpensslTls(tls::openssl_tls::Error::CreateSslConnector(e)))
Expand All @@ -258,16 +249,18 @@ impl ConfigExt for Config {
#[cfg(feature = "openssl-tls")]
fn openssl_https_connector(
&self,
identity: Option<Vec<u8>>,
) -> Result<hyper_openssl::client::legacy::HttpsConnector<HttpConnector>> {
let mut connector = HttpConnector::new();
connector.enforce_http(false);
self.openssl_https_connector_with_connector(connector)
self.openssl_https_connector_with_connector(connector, identity)
}

#[cfg(feature = "openssl-tls")]
fn openssl_https_connector_with_connector<H>(
&self,
connector: H,
identity: Option<Vec<u8>>,
) -> Result<hyper_openssl::client::legacy::HttpsConnector<H>>
where
H: tower::Service<http::Uri> + Send,
Expand All @@ -277,7 +270,7 @@ impl ConfigExt for Config {
{
let mut https = hyper_openssl::client::legacy::HttpsConnector::with_connector(
connector,
self.openssl_ssl_connector_builder()?,
self.openssl_ssl_connector_builder(identity)?,
)
.map_err(|e| Error::OpensslTls(tls::openssl_tls::Error::CreateHttpsConnector(e)))?;
if self.accept_invalid_certs {
Expand All @@ -291,22 +284,30 @@ impl ConfigExt for Config {
}

impl Config {
/// Retrieves an identity when an exec plugin returns a client certificate and key instead of a token.
///
/// This is necessary to check on TLS configuration vs tokens which can be added in as an AuthLayer.
///
/// # Returns
///
/// A tuple containing an optional vector of bytes representing the identity and an optional expiration date.

Check warning on line 294 in kube-client/src/client/config_ext.rs

View workflow job for this annotation

GitHub Actions / clippy_nightly

empty line after doc comment

warning: empty line after doc comment --> kube-client/src/client/config_ext.rs:293:5 | 293 | / /// A tuple containing an optional vector of bytes representing the identity and an optional expiration date. 294 | | | |_^ ... 299 | pub fn exec_identity_pem(&self) -> (Option<Vec<u8>>, Option<DateTime<Utc>>) { | --------------------------------------------------------------------------- the comment documents this method | = help: for further information visit https://rust-lang.github.io/rust-clippy/master/index.html#empty_line_after_doc_comments = note: `#[warn(clippy::empty_line_after_doc_comments)]` on by default = help: if the empty line is unintentional remove it help: if the doc comment should not document `exec_identity_pem` comment it out | 287 ~ // /// Retrieves an identity when an exec plugin returns a client certificate and key instead of a token. 288 ~ // /// 289 ~ // /// This is necessary to check on TLS configuration vs tokens which can be added in as an AuthLayer. 290 ~ // /// 291 ~ // /// # Returns 292 ~ // /// 293 ~ // /// A tuple containing an optional vector of bytes representing the identity and an optional expiration date. |
// This is necessary to retrieve an identity when an exec plugin
// returns a client certificate and key instead of a token.
// This has be to be checked on TLS configuration vs tokens
// which can be added in as an AuthLayer.
fn exec_identity_pem(&self) -> Option<Vec<u8>> {
pub fn exec_identity_pem(&self) -> (Option<Vec<u8>>, Option<DateTime<Utc>>) {
match Auth::try_from(&self.auth_info) {
Ok(Auth::Certificate(client_certificate_data, client_key_data)) => {
Ok(Auth::Certificate(client_certificate_data, client_key_data, expiratiom)) => {
const NEW_LINE: u8 = b'\n';

let mut buffer = client_key_data.expose_secret().as_bytes().to_vec();
buffer.push(NEW_LINE);
buffer.extend_from_slice(client_certificate_data.as_bytes());
buffer.push(NEW_LINE);
Some(buffer)
(Some(buffer), expiratiom)
}
_ => None,
_ => (None, None),
}
}
}
Loading

0 comments on commit 6c15783

Please sign in to comment.