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

add valid_until to Client #1676

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
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
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 @@
/// ```
#[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 @@
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 @@
/// ```
#[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 @@
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 @@
/// ```
#[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 @@
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 @@
}

#[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 @@
}

#[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 @@
#[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 @@
{
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 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
Loading