kube_client/client/
config_ext.rs

1use std::sync::Arc;
2
3use chrono::{DateTime, Utc};
4use http::{header::HeaderName, HeaderValue};
5#[cfg(feature = "openssl-tls")] use hyper::rt::{Read, Write};
6use hyper_util::client::legacy::connect::HttpConnector;
7use secrecy::ExposeSecret;
8use tower::{filter::AsyncFilterLayer, util::Either};
9
10#[cfg(any(feature = "rustls-tls", feature = "openssl-tls"))] use super::tls;
11use super::{
12    auth::Auth,
13    middleware::{AddAuthorizationLayer, AuthLayer, BaseUriLayer, ExtraHeadersLayer},
14};
15use crate::{Config, Error, Result};
16
17/// Extensions to [`Config`](crate::Config) for custom [`Client`](crate::Client).
18///
19/// See [`Client::new`](crate::Client::new) for an example.
20///
21/// This trait is sealed and cannot be implemented.
22pub trait ConfigExt: private::Sealed {
23    /// Layer to set the base URI of requests to the configured server.
24    fn base_uri_layer(&self) -> BaseUriLayer;
25
26    /// Optional layer to set up `Authorization` header depending on the config.
27    fn auth_layer(&self) -> Result<Option<AuthLayer>>;
28
29    /// Layer to add non-authn HTTP headers depending on the config.
30    fn extra_headers_layer(&self) -> Result<ExtraHeadersLayer>;
31
32    /// Create [`hyper_rustls::HttpsConnector`] based on config.
33    ///
34    /// # Example
35    ///
36    /// ```rust
37    /// # async fn doc() -> Result<(), Box<dyn std::error::Error>> {
38    /// # use kube::{client::{Body, ConfigExt}, Config};
39    /// # use hyper_util::rt::TokioExecutor;
40    /// let config = Config::infer().await?;
41    /// let https = config.rustls_https_connector()?;
42    /// let hyper_client: hyper_util::client::legacy::Client<_, Body> = hyper_util::client::legacy::Client::builder(TokioExecutor::new()).build(https);
43    /// # Ok(())
44    /// # }
45    /// ```
46    #[cfg_attr(docsrs, doc(cfg(feature = "rustls-tls")))]
47    #[cfg(feature = "rustls-tls")]
48    fn rustls_https_connector(&self) -> Result<hyper_rustls::HttpsConnector<HttpConnector>>;
49
50    /// Create [`hyper_rustls::HttpsConnector`] based on config and `connector`.
51    ///
52    /// # Example
53    ///
54    /// ```rust
55    /// # async fn doc() -> Result<(), Box<dyn std::error::Error>> {
56    /// # use kube::{client::{Body, ConfigExt}, Config};
57    /// # use hyper_util::{client::legacy::connect::HttpConnector, rt::TokioExecutor};
58    /// let config = Config::infer().await?;
59    /// let mut connector = HttpConnector::new();
60    /// connector.enforce_http(false);
61    /// let https = config.rustls_https_connector_with_connector(connector)?;
62    /// let hyper_client: hyper_util::client::legacy::Client<_, Body> = hyper_util::client::legacy::Client::builder(TokioExecutor::new()).build(https);
63    /// # Ok(())
64    /// # }
65    /// ```
66    #[cfg_attr(docsrs, doc(cfg(feature = "rustls-tls")))]
67    #[cfg(feature = "rustls-tls")]
68    fn rustls_https_connector_with_connector<H>(
69        &self,
70        connector: H,
71    ) -> Result<hyper_rustls::HttpsConnector<H>>;
72
73    /// Create [`rustls::ClientConfig`] based on config.
74    /// # Example
75    ///
76    /// ```rust
77    /// # async fn doc() -> Result<(), Box<dyn std::error::Error>> {
78    /// # use hyper_util::client::legacy::connect::HttpConnector;
79    /// # use kube::{client::ConfigExt, Config};
80    /// let config = Config::infer().await?;
81    /// let https = {
82    ///     let rustls_config = std::sync::Arc::new(config.rustls_client_config()?);
83    ///     let mut http = HttpConnector::new();
84    ///     http.enforce_http(false);
85    ///     hyper_rustls::HttpsConnector::from((http, rustls_config))
86    /// };
87    /// # Ok(())
88    /// # }
89    /// ```
90    #[cfg_attr(docsrs, doc(cfg(feature = "rustls-tls")))]
91    #[cfg(feature = "rustls-tls")]
92    fn rustls_client_config(&self) -> Result<rustls::ClientConfig>;
93
94    /// Create [`hyper_openssl::HttpsConnector`] based on config.
95    /// # Example
96    ///
97    /// ```rust
98    /// # async fn doc() -> Result<(), Box<dyn std::error::Error>> {
99    /// # use kube::{client::ConfigExt, Config};
100    /// let config = Config::infer().await?;
101    /// let https = config.openssl_https_connector()?;
102    /// # Ok(())
103    /// # }
104    /// ```
105    #[cfg_attr(docsrs, doc(cfg(feature = "openssl-tls")))]
106    #[cfg(feature = "openssl-tls")]
107    fn openssl_https_connector(&self)
108        -> Result<hyper_openssl::client::legacy::HttpsConnector<HttpConnector>>;
109
110    /// Create [`hyper_openssl::HttpsConnector`] based on config and `connector`.
111    /// # Example
112    ///
113    /// ```rust
114    /// # async fn doc() -> Result<(), Box<dyn std::error::Error>> {
115    /// # use hyper_util::client::legacy::connect::HttpConnector;
116    /// # use kube::{client::ConfigExt, Config};
117    /// let mut http = HttpConnector::new();
118    /// http.enforce_http(false);
119    /// let config = Config::infer().await?;
120    /// let https = config.openssl_https_connector_with_connector(http)?;
121    /// # Ok(())
122    /// # }
123    /// ```
124    #[cfg_attr(docsrs, doc(cfg(feature = "openssl-tls")))]
125    #[cfg(feature = "openssl-tls")]
126    fn openssl_https_connector_with_connector<H>(
127        &self,
128        connector: H,
129    ) -> Result<hyper_openssl::client::legacy::HttpsConnector<H>>
130    where
131        H: tower::Service<http::Uri> + Send,
132        H::Error: Into<Box<dyn std::error::Error + Send + Sync>>,
133        H::Future: Send + 'static,
134        H::Response: Read + Write + hyper_util::client::legacy::connect::Connection + Unpin;
135
136    /// Create [`openssl::ssl::SslConnectorBuilder`] based on config.
137    /// # Example
138    ///
139    /// ```rust
140    /// # async fn doc() -> Result<(), Box<dyn std::error::Error>> {
141    /// # use hyper_util::client::legacy::connect::HttpConnector;
142    /// # use kube::{client::ConfigExt, Client, Config};
143    /// let config = Config::infer().await?;
144    /// let https = {
145    ///     let mut http = HttpConnector::new();
146    ///     http.enforce_http(false);
147    ///     let ssl = config.openssl_ssl_connector_builder()?;
148    ///     hyper_openssl::client::legacy::HttpsConnector::with_connector(http, ssl)?
149    /// };
150    /// # Ok(())
151    /// # }
152    /// ```
153    #[cfg_attr(docsrs, doc(cfg(feature = "openssl-tls")))]
154    #[cfg(feature = "openssl-tls")]
155    fn openssl_ssl_connector_builder(&self) -> Result<openssl::ssl::SslConnectorBuilder>;
156}
157
158mod private {
159    pub trait Sealed {}
160    impl Sealed for super::Config {}
161}
162
163impl ConfigExt for Config {
164    fn base_uri_layer(&self) -> BaseUriLayer {
165        BaseUriLayer::new(self.cluster_url.clone())
166    }
167
168    fn auth_layer(&self) -> Result<Option<AuthLayer>> {
169        Ok(match Auth::try_from(&self.auth_info).map_err(Error::Auth)? {
170            Auth::None => None,
171            Auth::Basic(user, pass) => Some(AuthLayer(Either::Left(
172                AddAuthorizationLayer::basic(&user, pass.expose_secret()).as_sensitive(true),
173            ))),
174            Auth::Bearer(token) => Some(AuthLayer(Either::Left(
175                AddAuthorizationLayer::bearer(token.expose_secret()).as_sensitive(true),
176            ))),
177            Auth::RefreshableToken(refreshable) => {
178                Some(AuthLayer(Either::Right(AsyncFilterLayer::new(refreshable))))
179            }
180            Auth::Certificate(_client_certificate_data, _client_key_data, _) => None,
181        })
182    }
183
184    fn extra_headers_layer(&self) -> Result<ExtraHeadersLayer> {
185        let mut headers = self.headers.clone();
186        if let Some(impersonate_user) = &self.auth_info.impersonate {
187            headers.push((
188                HeaderName::from_static("impersonate-user"),
189                HeaderValue::from_str(impersonate_user)
190                    .map_err(http::Error::from)
191                    .map_err(Error::HttpError)?,
192            ));
193        }
194        if let Some(impersonate_groups) = &self.auth_info.impersonate_groups {
195            for group in impersonate_groups {
196                headers.push((
197                    HeaderName::from_static("impersonate-group"),
198                    HeaderValue::from_str(group)
199                        .map_err(http::Error::from)
200                        .map_err(Error::HttpError)?,
201                ));
202            }
203        }
204        Ok(ExtraHeadersLayer {
205            headers: Arc::new(headers),
206        })
207    }
208
209    #[cfg(feature = "rustls-tls")]
210    fn rustls_client_config(&self) -> Result<rustls::ClientConfig> {
211        let identity = self.exec_identity_pem().0.or_else(|| self.identity_pem());
212        tls::rustls_tls::rustls_client_config(
213            identity.as_deref(),
214            self.root_cert.as_deref(),
215            self.accept_invalid_certs,
216        )
217        .map_err(Error::RustlsTls)
218    }
219
220    #[cfg(feature = "rustls-tls")]
221    fn rustls_https_connector(&self) -> Result<hyper_rustls::HttpsConnector<HttpConnector>> {
222        let mut connector = HttpConnector::new();
223        connector.enforce_http(false);
224        self.rustls_https_connector_with_connector(connector)
225    }
226
227    #[cfg(feature = "rustls-tls")]
228    fn rustls_https_connector_with_connector<H>(
229        &self,
230        connector: H,
231    ) -> Result<hyper_rustls::HttpsConnector<H>> {
232        use hyper_rustls::FixedServerNameResolver;
233
234        use crate::client::tls::rustls_tls;
235
236        let rustls_config = self.rustls_client_config()?;
237        let mut builder = hyper_rustls::HttpsConnectorBuilder::new()
238            .with_tls_config(rustls_config)
239            .https_or_http();
240        if let Some(tsn) = self.tls_server_name.as_ref() {
241            builder = builder.with_server_name_resolver(FixedServerNameResolver::new(
242                tsn.clone()
243                    .try_into()
244                    .map_err(rustls_tls::Error::InvalidServerName)
245                    .map_err(Error::RustlsTls)?,
246            ));
247        }
248        Ok(builder.enable_http1().wrap_connector(connector))
249    }
250
251    #[cfg(feature = "openssl-tls")]
252    fn openssl_ssl_connector_builder(&self) -> Result<openssl::ssl::SslConnectorBuilder> {
253        let identity = self.exec_identity_pem().0.or_else(|| self.identity_pem());
254        // TODO: pass self.tls_server_name for openssl
255        tls::openssl_tls::ssl_connector_builder(identity.as_ref(), self.root_cert.as_ref())
256            .map_err(|e| Error::OpensslTls(tls::openssl_tls::Error::CreateSslConnector(e)))
257    }
258
259    #[cfg(feature = "openssl-tls")]
260    fn openssl_https_connector(
261        &self,
262    ) -> Result<hyper_openssl::client::legacy::HttpsConnector<HttpConnector>> {
263        let mut connector = HttpConnector::new();
264        connector.enforce_http(false);
265        self.openssl_https_connector_with_connector(connector)
266    }
267
268    #[cfg(feature = "openssl-tls")]
269    fn openssl_https_connector_with_connector<H>(
270        &self,
271        connector: H,
272    ) -> Result<hyper_openssl::client::legacy::HttpsConnector<H>>
273    where
274        H: tower::Service<http::Uri> + Send,
275        H::Error: Into<Box<dyn std::error::Error + Send + Sync>>,
276        H::Future: Send + 'static,
277        H::Response: Read + Write + hyper_util::client::legacy::connect::Connection + Unpin,
278    {
279        let mut https = hyper_openssl::client::legacy::HttpsConnector::with_connector(
280            connector,
281            self.openssl_ssl_connector_builder()?,
282        )
283        .map_err(|e| Error::OpensslTls(tls::openssl_tls::Error::CreateHttpsConnector(e)))?;
284        if self.accept_invalid_certs {
285            https.set_callback(|ssl, _uri| {
286                ssl.set_verify(openssl::ssl::SslVerifyMode::NONE);
287                Ok(())
288            });
289        }
290        Ok(https)
291    }
292}
293
294impl Config {
295    // This is necessary to retrieve an identity when an exec plugin
296    // returns a client certificate and key instead of a token.
297    // This has be to be checked on TLS configuration vs tokens
298    // which can be added in as an AuthLayer.
299    pub(crate) fn exec_identity_pem(&self) -> (Option<Vec<u8>>, Option<DateTime<Utc>>) {
300        match Auth::try_from(&self.auth_info) {
301            Ok(Auth::Certificate(client_certificate_data, client_key_data, expiratiom)) => {
302                const NEW_LINE: u8 = b'\n';
303
304                let mut buffer = client_key_data.expose_secret().as_bytes().to_vec();
305                buffer.push(NEW_LINE);
306                buffer.extend_from_slice(client_certificate_data.as_bytes());
307                buffer.push(NEW_LINE);
308                (Some(buffer), expiratiom)
309            }
310            _ => (None, None),
311        }
312    }
313}