kube_client/client/
config_ext.rs

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