From 37ad7f0c666360cc2ce2e7601cf5cf7297c229ea Mon Sep 17 00:00:00 2001 From: kazk Date: Tue, 25 May 2021 01:38:22 -0700 Subject: [PATCH 01/36] Expose methods to configure TLS connection - Add `Config::native_tls_connector` and `Config::rustls_client_config` - Remove the requirement of having `native-tls` or `rustls-tls` enabled when `client` is enabled. Allow one, both or none. - When both, the default Service will use `native-tls` because of #153. `rustls` can be still used with a custom client. Users will have an option to configure TLS at runtime. - When none, HTTP connector is used. - Note that `oauth` feature still requires tls feature. - Remove tls features from kube-runtime --- README.md | 2 +- examples/Cargo.toml | 6 +- kube-runtime/Cargo.toml | 7 +- kube/Cargo.toml | 1 - kube/src/client/mod.rs | 28 ++++-- kube/src/config/mod.rs | 14 +-- kube/src/config/tls.rs | 164 +++++++++++++++++++++++++++++++ kube/src/lib.rs | 18 ---- kube/src/service/auth/oauth.rs | 14 ++- kube/src/service/mod.rs | 2 - kube/src/service/tls.rs | 174 --------------------------------- 11 files changed, 207 insertions(+), 223 deletions(-) create mode 100644 kube/src/config/tls.rs delete mode 100644 kube/src/service/tls.rs diff --git a/README.md b/README.md index 8c08193dd..9eb941945 100644 --- a/README.md +++ b/README.md @@ -158,7 +158,7 @@ Kube has basic support ([with caveats](https://github.com/clux/kube-rs/issues?q= ```toml [dependencies] kube = { version = "0.55.0", default-features = false, features = ["rustls-tls"] } -kube-runtime = { version = "0.55.0", default-features = false, features = ["rustls-tls"] } +kube-runtime = { version = "0.55.0" } k8s-openapi = { version = "0.11.0", default-features = false, features = ["v1_20"] } ``` diff --git a/examples/Cargo.toml b/examples/Cargo.toml index 4867731b0..453de1a06 100644 --- a/examples/Cargo.toml +++ b/examples/Cargo.toml @@ -13,8 +13,8 @@ edition = "2018" default = ["native-tls", "schema", "kubederive", "ws"] kubederive = ["kube/derive"] # by default import kube-derive with its default features schema = ["kube-derive/schema"] # crd_derive_no_schema shows how to opt out -native-tls = ["kube/native-tls", "kube-runtime/native-tls"] -rustls-tls = ["kube/rustls-tls", "kube-runtime/rustls-tls"] +native-tls = ["kube/native-tls"] +rustls-tls = ["kube/rustls-tls"] ws = ["kube/ws"] [dev-dependencies] @@ -23,7 +23,7 @@ env_logger = "0.8.2" futures = "0.3.8" kube = { path = "../kube", version = "^0.55.0", default-features = false, features = ["admission"] } kube-derive = { path = "../kube-derive", version = "^0.55.0", default-features = false } # only needed to opt out of schema -kube-runtime = { path = "../kube-runtime", version = "^0.55.0", default-features = false } +kube-runtime = { path = "../kube-runtime", version = "^0.55.0" } kube-core = { path = "../kube-core", version = "^0.55.0", default-features = false } k8s-openapi = { version = "0.11.0", features = ["v1_20"], default-features = false } log = "0.4.11" diff --git a/kube-runtime/Cargo.toml b/kube-runtime/Cargo.toml index 130175d45..27c7fe9f9 100644 --- a/kube-runtime/Cargo.toml +++ b/kube-runtime/Cargo.toml @@ -14,7 +14,7 @@ edition = "2018" [dependencies] futures = "0.3.8" -kube = { path = "../kube", version = "^0.55.0", default-features = false } +kube = { path = "../kube", version = "^0.55.0", default-features = false, features = ["client"] } derivative = "2.1.1" serde = "1.0.118" smallvec = "1.6.0" @@ -28,11 +28,6 @@ tokio-util = { version = "0.6.0", features = ["time"] } version = "0.11.0" default-features = false -[features] -default = ["native-tls"] -native-tls = ["kube/native-tls"] -rustls-tls = ["kube/rustls-tls"] - [dev-dependencies] kube-derive = { path = "../kube-derive", version = "^0.55.0"} kube-core = { path = "../kube-core", version = "^0.55.0"} diff --git a/kube/Cargo.toml b/kube/Cargo.toml index 7ceedbbe9..54d064c21 100644 --- a/kube/Cargo.toml +++ b/kube/Cargo.toml @@ -54,7 +54,6 @@ tokio-native-tls = { version = "0.3.0", optional = true } tokio-rustls = { version = "0.22.0", features = ["dangerous_configuration"], optional = true } bytes = { version = "1.0.0", optional = true } tokio = { version = "1.0.1", features = ["time", "signal", "sync"], optional = true } -static_assertions = "1.1.0" kube-derive = { path = "../kube-derive", version = "^0.55.0", optional = true } kube-core = { path = "../kube-core", version = "^0.55.0"} jsonpath_lib = { version = "0.3.0", optional = true } diff --git a/kube/src/client/mod.rs b/kube/src/client/mod.rs index b0a28d05c..996e3782f 100644 --- a/kube/src/client/mod.rs +++ b/kube/src/client/mod.rs @@ -8,13 +8,13 @@ //! The [`Client`] can also be used with [`Discovery`](crate::Discovery) to dynamically //! retrieve the resources served by the kubernetes API. -use std::convert::{TryFrom, TryInto}; +use std::convert::TryFrom; use bytes::Bytes; use either::{Either, Left, Right}; use futures::{self, Stream, StreamExt, TryStream, TryStreamExt}; use http::{self, HeaderValue, Request, Response, StatusCode}; -use hyper::Body; +use hyper::{client::HttpConnector, Body}; use hyper_timeout::TimeoutConnector; use k8s_openapi::apimachinery::pkg::apis::meta::v1 as k8s_meta_v1; pub use kube_core::response::Status; @@ -28,13 +28,12 @@ use tokio_util::{ }; use tower::{buffer::Buffer, util::BoxService, BoxError, Service, ServiceBuilder, ServiceExt}; - #[cfg(feature = "gzip")] use crate::service::{accept_compressed, maybe_decompress}; use crate::{ api::WatchEvent, error::{ConfigError, ErrorResponse}, - service::{set_cluster_url, set_default_headers, AuthLayer, Authentication, HttpsConnector, LogRequest}, + service::{set_cluster_url, set_default_headers, AuthLayer, Authentication, LogRequest}, Config, Error, Result, }; @@ -423,8 +422,25 @@ impl TryFrom for Client { .map_response(maybe_decompress) .into_inner(); - let https: HttpsConnector<_> = config.try_into()?; - let mut connector = TimeoutConnector::new(https); + let mut connector = HttpConnector::new(); + connector.enforce_http(false); + + // Note that if both `native_tls` and `rustls` is enabled, `native_tls` is used by default. + // To use `rustls`, disable `native_tls` or create custom client. + // If tls features are not enabled, http connector will be used. + #[cfg(feature = "native-tls")] + let connector = hyper_tls::HttpsConnector::from(( + connector, + tokio_native_tls::TlsConnector::from(config.native_tls_connector()?), + )); + + #[cfg(all(not(feature = "native-tls"), feature = "rustls-tls"))] + let connector = hyper_rustls::HttpsConnector::from(( + connector, + std::sync::Arc::new(config.rustls_tls_client_config()?), + )); + + let mut connector = TimeoutConnector::new(connector); if let Some(timeout) = timeout { // reqwest's timeout is applied from when the request stars connecting until // the response body has finished. diff --git a/kube/src/config/mod.rs b/kube/src/config/mod.rs index 72a6d1047..6b37b0aaf 100644 --- a/kube/src/config/mod.rs +++ b/kube/src/config/mod.rs @@ -7,6 +7,7 @@ mod file_config; mod file_loader; mod incluster_config; +#[cfg(any(feature = "native-tls", feature = "rustls-tls"))] mod tls; mod utils; use crate::{error::ConfigError, Result}; @@ -35,9 +36,9 @@ pub struct Config { pub timeout: Option, /// Whether to accept invalid ceritifacts pub accept_invalid_certs: bool, - /// Client certs and key in PEM format and a password for a client to create `Identity` with. - /// Password is only used with `native_tls` to create a PKCS12 archive. - pub(crate) identity: Option<(Vec, String)>, + // TODO should keep client key and certificate separate. It's split later anyway. + /// Client certificate and private key in PEM. + identity_pem: Option>, /// Stores information to tell the cluster who you are. pub(crate) auth_info: AuthInfo, } @@ -56,7 +57,7 @@ impl Config { headers: HeaderMap::new(), timeout: Some(DEFAULT_TIMEOUT), accept_invalid_certs: false, - identity: None, + identity_pem: None, auth_info: AuthInfo::default(), } } @@ -114,7 +115,7 @@ impl Config { headers: HeaderMap::new(), timeout: Some(DEFAULT_TIMEOUT), accept_invalid_certs: false, - identity: None, + identity_pem: None, auth_info: AuthInfo { token: Some(token), ..Default::default() @@ -180,7 +181,7 @@ impl Config { headers: HeaderMap::new(), timeout: Some(DEFAULT_TIMEOUT), accept_invalid_certs, - identity: identity_pem.map(|i| (i, String::from(IDENTITY_PASSWORD))), + identity_pem, auth_info: loader.user, }) } @@ -189,7 +190,6 @@ impl Config { // https://github.com/clux/kube-rs/issues/146#issuecomment-590924397 /// Default Timeout const DEFAULT_TIMEOUT: Duration = Duration::from_secs(295); -const IDENTITY_PASSWORD: &str = " "; // temporary catalina hack for openssl only #[cfg(all(target_os = "macos", feature = "native-tls"))] diff --git a/kube/src/config/tls.rs b/kube/src/config/tls.rs new file mode 100644 index 000000000..ce33326f7 --- /dev/null +++ b/kube/src/config/tls.rs @@ -0,0 +1,164 @@ +use crate::Result; + +use super::Config; + +impl Config { + /// Create `native_tls::TlsConnector` + #[cfg(feature = "native-tls")] + pub fn native_tls_connector(&self) -> Result { + self::native_tls::native_tls_connector( + self.identity_pem.as_ref(), + self.root_cert.as_ref(), + self.accept_invalid_certs, + ) + } + + /// Create `rustls::ClientConfig` + #[cfg(feature = "rustls-tls")] + pub fn rustls_tls_client_config(&self) -> Result { + self::rustls_tls::rustls_client_config( + self.identity_pem.as_ref(), + self.root_cert.as_ref(), + self.accept_invalid_certs, + ) + } +} + + +#[cfg(feature = "native-tls")] +mod native_tls { + use tokio_native_tls::native_tls::{Certificate, Identity, TlsConnector}; + + use crate::{Error, Result}; + + const IDENTITY_PASSWORD: &str = " "; + + /// Create `native_tls::TlsConnector`. + pub fn native_tls_connector( + identity_pem: Option<&Vec>, + root_cert: Option<&Vec>>, + accept_invalid: bool, + ) -> Result { + let mut builder = TlsConnector::builder(); + if let Some(pem) = identity_pem { + let identity = pkcs12_from_pem(pem, IDENTITY_PASSWORD)?; + builder.identity( + Identity::from_pkcs12(&identity, IDENTITY_PASSWORD) + .map_err(|e| Error::SslError(format!("{}", e)))?, + ); + } + + if let Some(ders) = root_cert { + for der in ders { + builder.add_root_certificate( + Certificate::from_der(&der).map_err(|e| Error::SslError(format!("{}", e)))?, + ); + } + } + + if accept_invalid { + builder.danger_accept_invalid_certs(true); + } + + let connector = builder.build().map_err(|e| Error::SslError(format!("{}", e)))?; + Ok(connector) + } + + // TODO Replace this with pure Rust implementation to avoid depending on openssl on macOS and Win + fn pkcs12_from_pem(pem: &[u8], password: &str) -> Result> { + use openssl::{pkcs12::Pkcs12, pkey::PKey, x509::X509}; + let x509 = X509::from_pem(&pem)?; + let pkey = PKey::private_key_from_pem(&pem)?; + let p12 = Pkcs12::builder().build(password, "kubeconfig", &pkey, &x509)?; + let der = p12.to_der()?; + Ok(der) + } +} + +#[cfg(feature = "rustls-tls")] +mod rustls_tls { + use std::sync::Arc; + + use tokio_rustls::{ + rustls::{self, Certificate, ClientConfig, ServerCertVerified, ServerCertVerifier}, + webpki::DNSNameRef, + }; + + use crate::{Error, Result}; + + /// Create `rustls::ClientConfig`. + pub fn rustls_client_config( + identity_pem: Option<&Vec>, + root_cert: Option<&Vec>>, + accept_invalid: bool, + ) -> Result { + use rustls::internal::pemfile; + use std::io::Cursor; + + // Based on code from `reqwest` + let mut client_config = ClientConfig::new(); + if let Some(buf) = identity_pem { + let (key, certs) = { + let mut pem = Cursor::new(buf); + let certs = pemfile::certs(&mut pem) + .map_err(|_| Error::SslError("No valid certificate was found".into()))?; + pem.set_position(0); + + let mut sk = pemfile::pkcs8_private_keys(&mut pem) + .and_then(|pkcs8_keys| { + if pkcs8_keys.is_empty() { + Err(()) + } else { + Ok(pkcs8_keys) + } + }) + .or_else(|_| { + pem.set_position(0); + pemfile::rsa_private_keys(&mut pem) + }) + .map_err(|_| Error::SslError("No valid private key was found".into()))?; + + if let (Some(sk), false) = (sk.pop(), certs.is_empty()) { + (sk, certs) + } else { + return Err(Error::SslError("private key or certificate not found".into())); + } + }; + + client_config + .set_single_client_cert(certs, key) + .map_err(|e| Error::SslError(format!("{}", e)))?; + } + + if let Some(ders) = root_cert { + for der in ders { + client_config + .root_store + .add(&Certificate(der.to_owned())) + .map_err(|e| Error::SslError(format!("{}", e)))?; + } + } + + if accept_invalid { + client_config + .dangerous() + .set_certificate_verifier(Arc::new(NoCertificateVerification {})); + } + + Ok(client_config) + } + + struct NoCertificateVerification {} + + impl ServerCertVerifier for NoCertificateVerification { + fn verify_server_cert( + &self, + _roots: &rustls::RootCertStore, + _presented_certs: &[rustls::Certificate], + _dns_name: DNSNameRef<'_>, + _ocsp: &[u8], + ) -> Result { + Ok(ServerCertVerified::assertion()) + } + } +} diff --git a/kube/src/lib.rs b/kube/src/lib.rs index 760ea275e..d3c065753 100644 --- a/kube/src/lib.rs +++ b/kube/src/lib.rs @@ -74,24 +74,6 @@ #![deny(missing_docs)] #![deny(unsafe_code)] -#[macro_use] extern crate static_assertions; -assert_cfg!( - not(all(feature = "native-tls", feature = "rustls-tls")), - "Must use exactly one of native-tls or rustls-tls features" -); -assert_cfg!( - any( - all(feature = "native-tls", feature = "client"), - all(feature = "rustls-tls", feature = "client"), - all( - not(feature = "rustls-tls"), - not(feature = "native-tls"), - not(feature = "client") - ), - ), - "You must use a tls stack when using the client feature" -); - macro_rules! cfg_client { ($($item:item)*) => { $( diff --git a/kube/src/service/auth/oauth.rs b/kube/src/service/auth/oauth.rs index 1b73c82a9..fdca550c6 100644 --- a/kube/src/service/auth/oauth.rs +++ b/kube/src/service/auth/oauth.rs @@ -1,7 +1,5 @@ use std::{env, path::PathBuf}; -#[cfg(feature = "rustls-tls")] use hyper_rustls::HttpsConnector; -#[cfg(feature = "native-tls")] use hyper_tls::HttpsConnector; use tame_oauth::{ gcp::{ServiceAccountAccess, ServiceAccountInfo, TokenOrRequest}, Token, @@ -52,10 +50,16 @@ impl Gcp { Ok(TokenOrRequest::Request { request, scope_hash, .. }) => { - #[cfg(feature = "native-tls")] - let https = HttpsConnector::new(); + #[cfg(not(any(feature = "native-tls", feature = "rustls-tls")))] + compile_error!( + "At least one of native-tls or rustls-tls feature must be enabled to use oauth feature" + ); + // If both are enabled, we use rustls unlike `Client` because there's no need to support ip v4/6 subject matching. + // TODO Allow users to choose when both are enabled. #[cfg(feature = "rustls-tls")] - let https = HttpsConnector::with_native_roots(); + let https = hyper_rustls::HttpsConnector::with_native_roots(); + #[cfg(all(not(feature = "rustls-tls"), feature = "native-tls"))] + let https = hyper_tls::HttpsConnector::new(); let client = hyper::Client::builder().build::<_, hyper::Body>(https); let res = client diff --git a/kube/src/service/mod.rs b/kube/src/service/mod.rs index 0a455a25c..02ea2023e 100644 --- a/kube/src/service/mod.rs +++ b/kube/src/service/mod.rs @@ -4,7 +4,6 @@ mod auth; #[cfg(feature = "gzip")] mod compression; mod headers; mod log; -mod tls; mod url; #[cfg(feature = "gzip")] @@ -13,6 +12,5 @@ pub(crate) use self::{ auth::{AuthLayer, Authentication}, headers::set_default_headers, log::LogRequest, - tls::HttpsConnector, url::set_cluster_url, }; diff --git a/kube/src/service/tls.rs b/kube/src/service/tls.rs deleted file mode 100644 index 5c26807a9..000000000 --- a/kube/src/service/tls.rs +++ /dev/null @@ -1,174 +0,0 @@ -// Create `HttpsConnector` from `Config`. -// - hyper_tls::HttpsConnector from (hyper::client::HttpConnector, tokio_native_tls::TlsConnector) -// - hyper_rustls::HttpsConnector from (hyper::client::HttpConnector, Arc) - -pub use connector::HttpsConnector; - -#[cfg(feature = "native-tls")] -mod connector { - use std::convert::{TryFrom, TryInto}; - - use hyper::client::HttpConnector; - use tokio_native_tls::native_tls::{Certificate, Identity, TlsConnector}; - - use crate::{Config, Error, Result}; - - pub use hyper_tls::HttpsConnector; - use tokio_native_tls::TlsConnector as AsyncTlsConnector; - - impl TryFrom for HttpsConnector { - type Error = Error; - - fn try_from(config: Config) -> Result { - let mut http = HttpConnector::new(); - http.enforce_http(false); - let tls: AsyncTlsConnector = config.try_into()?; - Ok(HttpsConnector::from((http, tls))) - } - } - - impl TryFrom for AsyncTlsConnector { - type Error = Error; - - fn try_from(config: Config) -> Result { - let mut builder = TlsConnector::builder(); - if let Some((pem, identity_password)) = config.identity.as_ref() { - let identity = pkcs12_from_pem(pem, identity_password)?; - builder.identity( - Identity::from_pkcs12(&identity, identity_password) - .map_err(|e| Error::SslError(format!("{}", e)))?, - ); - } - - if let Some(ders) = config.root_cert { - for der in ders { - builder.add_root_certificate( - Certificate::from_der(&der).map_err(|e| Error::SslError(format!("{}", e)))?, - ); - } - } - - if config.accept_invalid_certs { - builder.danger_accept_invalid_certs(config.accept_invalid_certs); - } - - let connector = builder.build().map_err(|e| Error::SslError(format!("{}", e)))?; - Ok(AsyncTlsConnector::from(connector)) - } - } - - fn pkcs12_from_pem(pem: &[u8], password: &str) -> Result> { - use openssl::{pkcs12::Pkcs12, pkey::PKey, x509::X509}; - let x509 = X509::from_pem(&pem)?; - let pkey = PKey::private_key_from_pem(&pem)?; - let p12 = Pkcs12::builder().build(password, "kubeconfig", &pkey, &x509)?; - let der = p12.to_der()?; - Ok(der) - } -} - -#[cfg(feature = "rustls-tls")] -mod connector { - use std::{ - convert::{TryFrom, TryInto}, - sync::Arc, - }; - - use hyper::client::HttpConnector; - use tokio_rustls::{ - rustls::{self, Certificate, ClientConfig, ServerCertVerified, ServerCertVerifier}, - webpki::DNSNameRef, - }; - - use crate::{config::Config, Error, Result}; - - pub use hyper_rustls::HttpsConnector; - - impl TryFrom for HttpsConnector { - type Error = Error; - - fn try_from(config: Config) -> Result { - let mut http = HttpConnector::new(); - http.enforce_http(false); - let client_config: ClientConfig = config.try_into()?; - let client_config = Arc::new(client_config); - - Ok(HttpsConnector::from((http, client_config))) - } - } - - impl TryFrom for ClientConfig { - type Error = Error; - - fn try_from(config: Config) -> Result { - use rustls::internal::pemfile; - use std::io::Cursor; - - // Based on code from `reqwest` - let mut client_config = ClientConfig::new(); - if let Some((buf, _)) = config.identity.as_ref() { - let (key, certs) = { - let mut pem = Cursor::new(buf); - let certs = pemfile::certs(&mut pem) - .map_err(|_| Error::SslError("No valid certificate was found".into()))?; - pem.set_position(0); - - let mut sk = pemfile::pkcs8_private_keys(&mut pem) - .and_then(|pkcs8_keys| { - if pkcs8_keys.is_empty() { - Err(()) - } else { - Ok(pkcs8_keys) - } - }) - .or_else(|_| { - pem.set_position(0); - pemfile::rsa_private_keys(&mut pem) - }) - .map_err(|_| Error::SslError("No valid private key was found".into()))?; - - if let (Some(sk), false) = (sk.pop(), certs.is_empty()) { - (sk, certs) - } else { - return Err(Error::SslError("private key or certificate not found".into())); - } - }; - - client_config - .set_single_client_cert(certs, key) - .map_err(|e| Error::SslError(format!("{}", e)))?; - } - - if let Some(ders) = config.root_cert { - for der in ders { - client_config - .root_store - .add(&Certificate(der)) - .map_err(|e| Error::SslError(format!("{}", e)))?; - } - } - - if config.accept_invalid_certs { - client_config - .dangerous() - .set_certificate_verifier(Arc::new(NoCertificateVerification {})); - } - - Ok(client_config) - } - } - - struct NoCertificateVerification {} - - impl ServerCertVerifier for NoCertificateVerification { - fn verify_server_cert( - &self, - _roots: &rustls::RootCertStore, - _presented_certs: &[rustls::Certificate], - _dns_name: DNSNameRef<'_>, - _ocsp: &[u8], - ) -> Result { - Ok(ServerCertVerified::assertion()) - } - } -} From 196503f8bd6e0e82707e74bfb260e886f42a356e Mon Sep 17 00:00:00 2001 From: kazk Date: Tue, 25 May 2021 14:17:25 -0700 Subject: [PATCH 02/36] Depend on rustls directly instead of tokio-rustls Still a dependency of hyper-rustls, but we're not using tokio-rustls. Depend on rustls directly instead. --- README.md | 2 +- kube/Cargo.toml | 5 +++-- kube/src/config/tls.rs | 6 ++---- 3 files changed, 6 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index 9eb941945..a526af8c8 100644 --- a/README.md +++ b/README.md @@ -162,7 +162,7 @@ kube-runtime = { version = "0.55.0" } k8s-openapi = { version = "0.11.0", default-features = false, features = ["v1_20"] } ``` -This will pull in `hyper-rustls` and `tokio-rustls`. +This will pull in `rustls` and `hyper-rustls`. ## musl-libc Kube will work with [distroless](https://github.com/clux/controller-rs/blob/master/Dockerfile), [scratch](https://github.com/constellation-rs/constellation/blob/27dc89d0d0e34896fd37d638692e7dfe60a904fc/Dockerfile), and `alpine` (it's also possible to use alpine as a builder [with some caveats](https://github.com/clux/kube-rs/issues/331#issuecomment-715962188)). diff --git a/kube/Cargo.toml b/kube/Cargo.toml index 54d064c21..9a5c42dff 100644 --- a/kube/Cargo.toml +++ b/kube/Cargo.toml @@ -18,7 +18,7 @@ edition = "2018" [features] default = ["native-tls"] native-tls = ["client", "openssl", "hyper-tls", "tokio-native-tls"] -rustls-tls = ["client", "hyper-rustls", "tokio-rustls"] +rustls-tls = ["client", "rustls", "hyper-rustls", "webpki"] ws = ["client", "tokio-tungstenite", "rand", "kube-core/ws"] oauth = ["client", "tame-oauth"] gzip = ["client", "async-compression"] @@ -51,7 +51,8 @@ futures = { version = "0.3.8", optional = true } pem = { version = "0.8.2", optional = true } openssl = { version = "0.10.32", optional = true } tokio-native-tls = { version = "0.3.0", optional = true } -tokio-rustls = { version = "0.22.0", features = ["dangerous_configuration"], optional = true } +rustls = { version = "0.19.1", features = ["dangerous_configuration"], optional = true } +webpki = { version = "0.21.4", optional = true } bytes = { version = "1.0.0", optional = true } tokio = { version = "1.0.1", features = ["time", "signal", "sync"], optional = true } kube-derive = { path = "../kube-derive", version = "^0.55.0", optional = true } diff --git a/kube/src/config/tls.rs b/kube/src/config/tls.rs index ce33326f7..f1cdf07ca 100644 --- a/kube/src/config/tls.rs +++ b/kube/src/config/tls.rs @@ -79,10 +79,8 @@ mod native_tls { mod rustls_tls { use std::sync::Arc; - use tokio_rustls::{ - rustls::{self, Certificate, ClientConfig, ServerCertVerified, ServerCertVerifier}, - webpki::DNSNameRef, - }; + use rustls::{self, Certificate, ClientConfig, ServerCertVerified, ServerCertVerifier}; + use webpki::DNSNameRef; use crate::{Error, Result}; From 741df239878a429f0757a09ca866aabefb740ac4 Mon Sep 17 00:00:00 2001 From: kazk Date: Wed, 26 May 2021 03:23:12 -0700 Subject: [PATCH 03/36] Use rustls-pemfile instead of internal module --- kube/Cargo.toml | 3 ++- kube/src/config/tls.rs | 40 ++++++++++++++++++++++++++++++++-------- 2 files changed, 34 insertions(+), 9 deletions(-) diff --git a/kube/Cargo.toml b/kube/Cargo.toml index 9a5c42dff..6e8a67372 100644 --- a/kube/Cargo.toml +++ b/kube/Cargo.toml @@ -18,7 +18,7 @@ edition = "2018" [features] default = ["native-tls"] native-tls = ["client", "openssl", "hyper-tls", "tokio-native-tls"] -rustls-tls = ["client", "rustls", "hyper-rustls", "webpki"] +rustls-tls = ["client", "rustls", "rustls-pemfile", "hyper-rustls", "webpki"] ws = ["client", "tokio-tungstenite", "rand", "kube-core/ws"] oauth = ["client", "tame-oauth"] gzip = ["client", "async-compression"] @@ -52,6 +52,7 @@ pem = { version = "0.8.2", optional = true } openssl = { version = "0.10.32", optional = true } tokio-native-tls = { version = "0.3.0", optional = true } rustls = { version = "0.19.1", features = ["dangerous_configuration"], optional = true } +rustls-pemfile = { version = "0.2.1", optional = true } webpki = { version = "0.21.4", optional = true } bytes = { version = "1.0.0", optional = true } tokio = { version = "1.0.1", features = ["time", "signal", "sync"], optional = true } diff --git a/kube/src/config/tls.rs b/kube/src/config/tls.rs index f1cdf07ca..3a350f785 100644 --- a/kube/src/config/tls.rs +++ b/kube/src/config/tls.rs @@ -90,7 +90,6 @@ mod rustls_tls { root_cert: Option<&Vec>>, accept_invalid: bool, ) -> Result { - use rustls::internal::pemfile; use std::io::Cursor; // Based on code from `reqwest` @@ -98,21 +97,46 @@ mod rustls_tls { if let Some(buf) = identity_pem { let (key, certs) = { let mut pem = Cursor::new(buf); - let certs = pemfile::certs(&mut pem) + let certs = rustls_pemfile::certs(&mut pem) + .and_then(|certs| { + if certs.is_empty() { + Err(std::io::Error::new( + std::io::ErrorKind::NotFound, + "No X.509 Certificates Found", + )) + } else { + Ok(certs.into_iter().map(rustls::Certificate).collect::>()) + } + }) .map_err(|_| Error::SslError("No valid certificate was found".into()))?; pem.set_position(0); - let mut sk = pemfile::pkcs8_private_keys(&mut pem) - .and_then(|pkcs8_keys| { - if pkcs8_keys.is_empty() { - Err(()) + // TODO Support EC Private Key to support k3d. Need to convert to PKCS#8 or RSA (PKCS#1). + // `openssl pkcs8 -topk8 -nocrypt -in ec.pem -out pkcs8.pem` + // https://wiki.openssl.org/index.php/Command_Line_Elliptic_Curve_Operations#EC_Private_Key_File_Formats + let mut sk = rustls_pemfile::pkcs8_private_keys(&mut pem) + .and_then(|keys| { + if keys.is_empty() { + Err(std::io::Error::new( + std::io::ErrorKind::NotFound, + "No PKCS8 Key Found", + )) } else { - Ok(pkcs8_keys) + Ok(keys.into_iter().map(rustls::PrivateKey).collect::>()) } }) .or_else(|_| { pem.set_position(0); - pemfile::rsa_private_keys(&mut pem) + rustls_pemfile::rsa_private_keys(&mut pem).and_then(|keys| { + if keys.is_empty() { + Err(std::io::Error::new( + std::io::ErrorKind::NotFound, + "No RSA Key Found", + )) + } else { + Ok(keys.into_iter().map(rustls::PrivateKey).collect::>()) + } + }) }) .map_err(|_| Error::SslError("No valid private key was found".into()))?; From d4361124853b292ad4b2332fd828c3d58145a38f Mon Sep 17 00:00:00 2001 From: kazk Date: Wed, 26 May 2021 03:25:59 -0700 Subject: [PATCH 04/36] Add custom client example --- examples/Cargo.toml | 19 ++++++-- examples/custom_client.rs | 100 ++++++++++++++++++++++++++++++++++++++ kube/src/lib.rs | 3 ++ kube/src/service/mod.rs | 2 +- 4 files changed, 118 insertions(+), 6 deletions(-) create mode 100644 examples/custom_client.rs diff --git a/examples/Cargo.toml b/examples/Cargo.toml index 453de1a06..653db3a83 100644 --- a/examples/Cargo.toml +++ b/examples/Cargo.toml @@ -10,13 +10,19 @@ publish = false edition = "2018" [features] -default = ["native-tls", "schema", "kubederive", "ws"] +default = ["native-tls", "rustls-tls", "schema", "kubederive", "ws"] kubederive = ["kube/derive"] # by default import kube-derive with its default features schema = ["kube-derive/schema"] # crd_derive_no_schema shows how to opt out -native-tls = ["kube/native-tls"] -rustls-tls = ["kube/rustls-tls"] +native-tls = ["kube/native-tls", "hyper-tls", "tokio-native-tls"] +rustls-tls = ["kube/rustls-tls", "hyper-rustls"] ws = ["kube/ws"] +[dependencies] +tokio-util = "0.6.0" +hyper-tls = { version = "0.5.0", optional = true } +tokio-native-tls = { version = "0.3.0", optional = true } +hyper-rustls = { version = "0.22.1", optional = true } + [dev-dependencies] anyhow = "1.0.37" env_logger = "0.8.2" @@ -43,6 +49,8 @@ tracing-subscriber = "0.2" warp = { version = "0.3", features = ["tls"] } http = "0.2.3" json-patch = "0.2.6" +tower = { version = "0.4.6" } +hyper = { version = "0.14.2", features = ["client", "http1", "stream", "tcp"] } [[example]] name = "configmapgen_controller" @@ -160,5 +168,6 @@ path = "secret_reflector.rs" name = "admission_controller" path = "admission_controller.rs" -[dependencies] -tokio-util = "0.6.0" +[[example]] +name = "custom_client" +path = "custom_client.rs" diff --git a/examples/custom_client.rs b/examples/custom_client.rs new file mode 100644 index 000000000..2ef23f821 --- /dev/null +++ b/examples/custom_client.rs @@ -0,0 +1,100 @@ +// Run with `cargo run --example custom_client --no-default-features --features native-tls,rustls-tls` +#[macro_use] extern crate log; +use futures::{StreamExt, TryStreamExt}; +use hyper::client::HttpConnector; +use hyper_rustls::HttpsConnector as RustlsHttpsConnector; +use hyper_tls::HttpsConnector; +use k8s_openapi::api::core::v1::Pod; +use serde_json::json; +use tokio_native_tls::TlsConnector; +use tower::ServiceBuilder; + +use kube::{ + api::{Api, DeleteParams, ListParams, PostParams, ResourceExt, WatchEvent}, + Client, Config, +}; + +#[tokio::main] +async fn main() -> anyhow::Result<()> { + std::env::set_var("RUST_LOG", "info,kube=debug"); + env_logger::init(); + + let config = Config::infer().await?; + let cluster_url = config.cluster_url.clone(); + let common = ServiceBuilder::new() + .map_request(move |r| kube::set_cluster_url(r, &cluster_url)) + .map_err(|e: hyper::Error| e.into()) + .into_inner(); + let mut http = HttpConnector::new(); + http.enforce_http(false); + + // Pick TLS at runtime + let use_rustls = std::env::var("USE_RUSTLS").map(|s| s == "1").unwrap_or(false); + let client = if use_rustls { + let https = + RustlsHttpsConnector::from((http, std::sync::Arc::new(config.rustls_tls_client_config()?))); + let inner = ServiceBuilder::new() + .layer(common) + .service(hyper::Client::builder().build(https)); + Client::new(inner) + } else { + let https = HttpsConnector::from((http, TlsConnector::from(config.native_tls_connector()?))); + let inner = ServiceBuilder::new() + .layer(common) + .service(hyper::Client::builder().build(https)); + Client::new(inner) + }; + + // Manage pods + let pods: Api = Api::namespaced(client, "default"); + // Create pod + let p: Pod = serde_json::from_value(json!({ + "apiVersion": "v1", + "kind": "Pod", + "metadata": { "name": "example" }, + "spec": { "containers": [{ "name": "example", "image": "alpine" }] } + }))?; + + let pp = PostParams::default(); + match pods.create(&pp, &p).await { + Ok(o) => { + let name = o.name(); + assert_eq!(p.name(), name); + info!("Created {}", name); + std::thread::sleep(std::time::Duration::from_millis(5_000)); + } + Err(kube::Error::Api(ae)) => assert_eq!(ae.code, 409), // if you skipped delete, for instance + Err(e) => return Err(e.into()), + } + + // Watch it phase for a few seconds + let lp = ListParams::default() + .fields(&format!("metadata.name={}", "example")) + .timeout(10); + let mut stream = pods.watch(&lp, "0").await?.boxed(); + while let Some(status) = stream.try_next().await? { + match status { + WatchEvent::Added(o) => info!("Added {}", o.name()), + WatchEvent::Modified(o) => { + let s = o.status.as_ref().expect("status exists on pod"); + let phase = s.phase.clone().unwrap_or_default(); + info!("Modified: {} with phase: {}", o.name(), phase); + } + WatchEvent::Deleted(o) => info!("Deleted {}", o.name()), + WatchEvent::Error(e) => error!("Error {}", e), + _ => {} + } + } + + if let Some(spec) = &pods.get("example").await?.spec { + assert_eq!(spec.containers[0].name, "example"); + } + + pods.delete("example", &DeleteParams::default()) + .await? + .map_left(|pdel| { + assert_eq!(pdel.name(), "example"); + }); + + Ok(()) +} diff --git a/kube/src/lib.rs b/kube/src/lib.rs index d3c065753..0b87446a6 100644 --- a/kube/src/lib.rs +++ b/kube/src/lib.rs @@ -108,6 +108,9 @@ cfg_client! { pub mod discovery; pub mod client; pub(crate) mod service; + // Export this for examples for now. + #[doc(hidden)] + pub use service::set_cluster_url; #[doc(inline)] pub use api::Api; diff --git a/kube/src/service/mod.rs b/kube/src/service/mod.rs index 02ea2023e..71747b8dd 100644 --- a/kube/src/service/mod.rs +++ b/kube/src/service/mod.rs @@ -8,9 +8,9 @@ mod url; #[cfg(feature = "gzip")] pub(crate) use self::compression::{accept_compressed, maybe_decompress}; +pub use self::url::set_cluster_url; pub(crate) use self::{ auth::{AuthLayer, Authentication}, headers::set_default_headers, log::LogRequest, - url::set_cluster_url, }; From a0efcfa746fa97ace2340673c9a16546400f1ee6 Mon Sep 17 00:00:00 2001 From: kazk Date: Wed, 26 May 2021 11:54:44 -0700 Subject: [PATCH 05/36] Remove `client` from `native-tls` and `rust-tls` `config` + `native-tls`/`rustls-tls` can be used independently. For example, to create a simple HTTP client. --- README.md | 2 +- examples/Cargo.toml | 4 ++-- kube/Cargo.toml | 6 +++--- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index a526af8c8..1edf7a70a 100644 --- a/README.md +++ b/README.md @@ -157,7 +157,7 @@ Kube has basic support ([with caveats](https://github.com/clux/kube-rs/issues?q= ```toml [dependencies] -kube = { version = "0.55.0", default-features = false, features = ["rustls-tls"] } +kube = { version = "0.55.0", default-features = false, features = ["client", "rustls-tls"] } kube-runtime = { version = "0.55.0" } k8s-openapi = { version = "0.11.0", default-features = false, features = ["v1_20"] } ``` diff --git a/examples/Cargo.toml b/examples/Cargo.toml index 653db3a83..fdd00f5ac 100644 --- a/examples/Cargo.toml +++ b/examples/Cargo.toml @@ -13,8 +13,8 @@ edition = "2018" default = ["native-tls", "rustls-tls", "schema", "kubederive", "ws"] kubederive = ["kube/derive"] # by default import kube-derive with its default features schema = ["kube-derive/schema"] # crd_derive_no_schema shows how to opt out -native-tls = ["kube/native-tls", "hyper-tls", "tokio-native-tls"] -rustls-tls = ["kube/rustls-tls", "hyper-rustls"] +native-tls = ["kube/client", "kube/native-tls", "hyper-tls", "tokio-native-tls"] +rustls-tls = ["kube/client", "kube/rustls-tls", "hyper-rustls"] ws = ["kube/ws"] [dependencies] diff --git a/kube/Cargo.toml b/kube/Cargo.toml index 6e8a67372..15916694c 100644 --- a/kube/Cargo.toml +++ b/kube/Cargo.toml @@ -16,9 +16,9 @@ categories = ["web-programming::http-client"] edition = "2018" [features] -default = ["native-tls"] -native-tls = ["client", "openssl", "hyper-tls", "tokio-native-tls"] -rustls-tls = ["client", "rustls", "rustls-pemfile", "hyper-rustls", "webpki"] +default = ["client", "native-tls"] +native-tls = ["openssl", "hyper-tls", "tokio-native-tls"] +rustls-tls = ["rustls", "rustls-pemfile", "hyper-rustls", "webpki"] ws = ["client", "tokio-tungstenite", "rand", "kube-core/ws"] oauth = ["client", "tame-oauth"] gzip = ["client", "async-compression"] From cb081ae5ebac6fd4e292b7f09c5745c4a073c8b3 Mon Sep 17 00:00:00 2001 From: kazk Date: Wed, 26 May 2021 13:50:48 -0700 Subject: [PATCH 06/36] Change Service's Error to `Into` --- examples/custom_client.rs | 1 - kube/src/client/mod.rs | 10 +++++----- 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/examples/custom_client.rs b/examples/custom_client.rs index 2ef23f821..2af5037c5 100644 --- a/examples/custom_client.rs +++ b/examples/custom_client.rs @@ -23,7 +23,6 @@ async fn main() -> anyhow::Result<()> { let cluster_url = config.cluster_url.clone(); let common = ServiceBuilder::new() .map_request(move |r| kube::set_cluster_url(r, &cluster_url)) - .map_err(|e: hyper::Error| e.into()) .into_inner(); let mut http = HttpConnector::new(); http.enforce_http(false); diff --git a/kube/src/client/mod.rs b/kube/src/client/mod.rs index 996e3782f..b02383154 100644 --- a/kube/src/client/mod.rs +++ b/kube/src/client/mod.rs @@ -59,22 +59,22 @@ impl Client { /// Create and initialize a [`Client`] using the given `Service`. /// /// Use [`Client::try_from`](Self::try_from) to create with a [`Config`]. - pub fn new(service: S) -> Self + pub fn new>(service: S) -> Self where - S: Service, Response = Response, Error = BoxError> + Send + 'static, + S: Service, Response = Response, Error = E> + Send + 'static, S::Future: Send + 'static, { Self::new_with_default_ns(service, "default") } /// Create and initialize a [`Client`] using the given `Service` and the default namespace. - fn new_with_default_ns>(service: S, default_ns: T) -> Self + fn new_with_default_ns, T: Into>(service: S, default_ns: T) -> Self where - S: Service, Response = Response, Error = BoxError> + Send + 'static, + S: Service, Response = Response, Error = E> + Send + 'static, S::Future: Send + 'static, { Self { - inner: Buffer::new(BoxService::new(service), 1024), + inner: Buffer::new(BoxService::new(service.map_err(|e| e.into())), 1024), default_ns: default_ns.into(), } } From 93e6c65852f560cc4f78d29dcb2b9163f7bf2bb2 Mon Sep 17 00:00:00 2001 From: kazk Date: Wed, 26 May 2021 19:39:02 -0700 Subject: [PATCH 07/36] Always use `http::Uri` instead of `url::Url` --- kube-core/Cargo.toml | 2 +- kube-core/src/params.rs | 2 +- kube-core/src/request.rs | 22 ++++++------- kube-core/src/subresource.rs | 10 +++--- kube/Cargo.toml | 1 - kube/src/config/mod.rs | 10 +++--- kube/src/error.rs | 9 ++--- kube/src/service/url.rs | 64 ++++++++++++++++++------------------ 8 files changed, 58 insertions(+), 62 deletions(-) diff --git a/kube-core/Cargo.toml b/kube-core/Cargo.toml index 50c13f516..ee339706e 100644 --- a/kube-core/Cargo.toml +++ b/kube-core/Cargo.toml @@ -21,7 +21,7 @@ serde = { version = "1.0.118", features = ["derive"] } serde_json = "1.0.61" thiserror = "1.0.23" once_cell = "1.7.2" -url = "2.2.0" +form_urlencoded = "1.0.1" http = "0.2.2" json-patch = { version = "0.2.6", optional = true } diff --git a/kube-core/src/params.rs b/kube-core/src/params.rs index e291e0843..5877abf69 100644 --- a/kube-core/src/params.rs +++ b/kube-core/src/params.rs @@ -287,7 +287,7 @@ impl PatchParams { Ok(()) } - pub(crate) fn populate_qp(&self, qp: &mut url::form_urlencoded::Serializer) { + pub(crate) fn populate_qp(&self, qp: &mut form_urlencoded::Serializer) { if self.dry_run { qp.append_pair("dryRun", "All"); } diff --git a/kube-core/src/request.rs b/kube-core/src/request.rs index a14dc1087..3e8534626 100644 --- a/kube-core/src/request.rs +++ b/kube-core/src/request.rs @@ -27,7 +27,7 @@ impl Request { /// List a collection of a resource pub fn list(&self, lp: &ListParams) -> Result>> { let target = format!("{}?", self.url_path); - let mut qp = url::form_urlencoded::Serializer::new(target); + let mut qp = form_urlencoded::Serializer::new(target); if let Some(fields) = &lp.field_selector { qp.append_pair("fieldSelector", &fields); @@ -50,7 +50,7 @@ impl Request { /// Watch a resource at a given version pub fn watch(&self, lp: &ListParams, ver: &str) -> Result>> { let target = format!("{}?", self.url_path); - let mut qp = url::form_urlencoded::Serializer::new(target); + let mut qp = form_urlencoded::Serializer::new(target); lp.validate()?; if lp.limit.is_some() { return Err(Error::RequestValidation( @@ -86,7 +86,7 @@ impl Request { /// Get a single instance pub fn get(&self, name: &str) -> Result>> { let target = format!("{}/{}", self.url_path, name); - let mut qp = url::form_urlencoded::Serializer::new(target); + let mut qp = form_urlencoded::Serializer::new(target); let urlstr = qp.finish(); let req = http::Request::get(urlstr); req.body(vec![]).map_err(Error::HttpError) @@ -96,7 +96,7 @@ impl Request { pub fn create(&self, pp: &PostParams, data: Vec) -> Result>> { pp.validate()?; let target = format!("{}?", self.url_path); - let mut qp = url::form_urlencoded::Serializer::new(target); + let mut qp = form_urlencoded::Serializer::new(target); if pp.dry_run { qp.append_pair("dryRun", "All"); } @@ -108,7 +108,7 @@ impl Request { /// Delete an instance of a resource pub fn delete(&self, name: &str, dp: &DeleteParams) -> Result>> { let target = format!("{}/{}?", self.url_path, name); - let mut qp = url::form_urlencoded::Serializer::new(target); + let mut qp = form_urlencoded::Serializer::new(target); let urlstr = qp.finish(); let body = serde_json::to_vec(&dp)?; let req = http::Request::delete(urlstr); @@ -118,7 +118,7 @@ impl Request { /// Delete a collection of a resource pub fn delete_collection(&self, dp: &DeleteParams, lp: &ListParams) -> Result>> { let target = format!("{}?", self.url_path); - let mut qp = url::form_urlencoded::Serializer::new(target); + let mut qp = form_urlencoded::Serializer::new(target); if let Some(fields) = &lp.field_selector { qp.append_pair("fieldSelector", &fields); } @@ -142,7 +142,7 @@ impl Request { ) -> Result>> { pp.validate(patch)?; let target = format!("{}/{}?", self.url_path, name); - let mut qp = url::form_urlencoded::Serializer::new(target); + let mut qp = form_urlencoded::Serializer::new(target); pp.populate_qp(&mut qp); let urlstr = qp.finish(); @@ -158,7 +158,7 @@ impl Request { /// Requires `metadata.resourceVersion` set in data pub fn replace(&self, name: &str, pp: &PostParams, data: Vec) -> Result>> { let target = format!("{}/{}?", self.url_path, name); - let mut qp = url::form_urlencoded::Serializer::new(target); + let mut qp = form_urlencoded::Serializer::new(target); if pp.dry_run { qp.append_pair("dryRun", "All"); } @@ -173,7 +173,7 @@ impl Request { /// Get an instance of the subresource pub fn get_subresource(&self, subresource_name: &str, name: &str) -> Result>> { let target = format!("{}/{}/{}", self.url_path, name, subresource_name); - let mut qp = url::form_urlencoded::Serializer::new(target); + let mut qp = form_urlencoded::Serializer::new(target); let urlstr = qp.finish(); let req = http::Request::get(urlstr); req.body(vec![]).map_err(Error::HttpError) @@ -189,7 +189,7 @@ impl Request { ) -> Result>> { pp.validate(patch)?; let target = format!("{}/{}/{}?", self.url_path, name, subresource_name); - let mut qp = url::form_urlencoded::Serializer::new(target); + let mut qp = form_urlencoded::Serializer::new(target); pp.populate_qp(&mut qp); let urlstr = qp.finish(); @@ -209,7 +209,7 @@ impl Request { data: Vec, ) -> Result>> { let target = format!("{}/{}/{}?", self.url_path, name, subresource_name); - let mut qp = url::form_urlencoded::Serializer::new(target); + let mut qp = form_urlencoded::Serializer::new(target); if pp.dry_run { qp.append_pair("dryRun", "All"); } diff --git a/kube-core/src/subresource.rs b/kube-core/src/subresource.rs index 098d9aab2..ca343a0b9 100644 --- a/kube-core/src/subresource.rs +++ b/kube-core/src/subresource.rs @@ -42,7 +42,7 @@ impl Request { /// Get a pod logs pub fn logs(&self, name: &str, lp: &LogParams) -> Result>> { let target = format!("{}/{}/log?", self.url_path, name); - let mut qp = url::form_urlencoded::Serializer::new(target); + let mut qp = form_urlencoded::Serializer::new(target); if let Some(container) = &lp.container { qp.append_pair("container", &container); @@ -102,7 +102,7 @@ impl Request { // This is technically identical to Request::create, but different url let pp = &ep.post_options; pp.validate()?; - let mut qp = url::form_urlencoded::Serializer::new(target); + let mut qp = form_urlencoded::Serializer::new(target); if pp.dry_run { qp.append_pair("dryRun", "All"); } @@ -263,7 +263,7 @@ impl AttachParams { Ok(()) } - fn append_to_url_serializer(&self, qp: &mut url::form_urlencoded::Serializer) { + fn append_to_url_serializer(&self, qp: &mut form_urlencoded::Serializer) { if self.stdin { qp.append_pair("stdin", "true"); } @@ -290,7 +290,7 @@ impl Request { ap.validate()?; let target = format!("{}/{}/attach?", self.url_path, name); - let mut qp = url::form_urlencoded::Serializer::new(target); + let mut qp = form_urlencoded::Serializer::new(target); ap.append_to_url_serializer(&mut qp); let req = http::Request::get(qp.finish()); @@ -313,7 +313,7 @@ impl Request { ap.validate()?; let target = format!("{}/{}/exec?", self.url_path, name); - let mut qp = url::form_urlencoded::Serializer::new(target); + let mut qp = form_urlencoded::Serializer::new(target); ap.append_to_url_serializer(&mut qp); for c in command.into_iter() { diff --git a/kube/Cargo.toml b/kube/Cargo.toml index 15916694c..14fb0473e 100644 --- a/kube/Cargo.toml +++ b/kube/Cargo.toml @@ -44,7 +44,6 @@ serde = { version = "1.0.118", features = ["derive"] } serde_json = "1.0.61" serde_yaml = { version = "0.8.17", optional = true } http = "0.2.2" -url = "2.2.0" either = { version = "1.6.1", optional = true } thiserror = "1.0.23" futures = { version = "0.3.8", optional = true } diff --git a/kube/src/config/mod.rs b/kube/src/config/mod.rs index 6b37b0aaf..0c1b6524a 100644 --- a/kube/src/config/mod.rs +++ b/kube/src/config/mod.rs @@ -23,7 +23,7 @@ use std::time::Duration; #[derive(Debug, Clone)] pub struct Config { /// The configured cluster url - pub cluster_url: url::Url, + pub cluster_url: http::Uri, /// The configured default namespace pub default_ns: String, /// The configured root certificate @@ -49,7 +49,7 @@ impl Config { /// /// Most likely you want to use [`Config::infer`] to infer the config from /// the environment. - pub fn new(cluster_url: url::Url) -> Self { + pub fn new(cluster_url: http::Uri) -> Self { Self { cluster_url, default_ns: String::from("default"), @@ -96,7 +96,7 @@ impl Config { hostenv: incluster_config::SERVICE_HOSTENV, portenv: incluster_config::SERVICE_PORTENV, })?; - let cluster_url = url::Url::parse(&cluster_url)?; + let cluster_url = cluster_url.parse::()?; let default_ns = incluster_config::load_default_ns() .map_err(Box::new) @@ -143,7 +143,7 @@ impl Config { } async fn new_from_loader(loader: ConfigLoader) -> Result { - let cluster_url = url::Url::parse(&loader.cluster.server)?; + let cluster_url = loader.cluster.server.parse::()?; let default_ns = loader .current_context @@ -245,6 +245,6 @@ mod tests { std::fs::write(file.path(), cfgraw).unwrap(); std::env::set_var("KUBECONFIG", file.path()); let kubeconfig = Config::infer().await.unwrap(); - assert_eq!(kubeconfig.cluster_url.into_string(), "https://0.0.0.0:6443/"); + assert_eq!(kubeconfig.cluster_url, "https://0.0.0.0:6443/"); } } diff --git a/kube/src/error.rs b/kube/src/error.rs index f756f0483..7998992fa 100644 --- a/kube/src/error.rs +++ b/kube/src/error.rs @@ -47,9 +47,9 @@ pub enum Error { #[error("HttpError: {0}")] HttpError(#[from] http::Error), - /// Url conversion error - #[error("InternalUrlError: {0}")] - InternalUrlError(#[from] url::ParseError), + /// Failed to construct a URI. + #[error(transparent)] + InvalidUri(#[from] http::uri::InvalidUri), /// Common error case when requesting parsing into own structs #[error("Error deserializing response")] @@ -166,9 +166,6 @@ pub enum ConfigError { #[error("Unable to load in cluster token: {0}")] InvalidInClusterToken(#[source] Box), - #[error("Malformed url: {0}")] - MalformedUrl(#[from] url::ParseError), - #[error("exec-plugin response did not contain a status")] ExecPluginFailed, diff --git a/kube/src/service/url.rs b/kube/src/service/url.rs index 8321970aa..a48b845b5 100644 --- a/kube/src/service/url.rs +++ b/kube/src/service/url.rs @@ -1,36 +1,43 @@ -use http::Request; +use http::{uri, Request}; use hyper::Body; /// Set cluster URL. -pub fn set_cluster_url(req: Request, url: &url::Url) -> Request { +pub fn set_cluster_url(req: Request, base_uri: &http::Uri) -> Request { let (mut parts, body) = req.into_parts(); - let pandq = parts.uri.path_and_query().expect("valid path+query from kube"); - parts.uri = finalize_url(url, &pandq).parse().expect("valid URL"); + let request_pandq = parts.uri.path_and_query().expect("nonempty path+query"); + parts.uri = finalize_url(base_uri, request_pandq); Request::from_parts(parts, body) } -/// An internal url joiner to deal with the two different interfaces -/// -/// - api module produces a http::Uri which we can turn into a PathAndQuery (has a leading slash by construction) -/// - config module produces a url::Url from user input (sometimes contains path segments) -/// -/// This deals with that in a pretty easy way (tested below) -fn finalize_url(cluster_url: &url::Url, request_pandq: &http::uri::PathAndQuery) -> String { - let base = cluster_url.as_str().trim_end_matches('/'); // pandq always starts with a slash - format!("{}{}", base, request_pandq) +// Join base URI and Path+Query, preserving any path in the base. +fn finalize_url(base_uri: &http::Uri, request_pandq: &uri::PathAndQuery) -> http::Uri { + let mut builder = uri::Builder::new(); + if let Some(scheme) = base_uri.scheme() { + builder = builder.scheme(scheme.as_str()); + } + if let Some(authority) = base_uri.authority() { + builder = builder.authority(authority.as_str()); + } + if let Some(pandq) = base_uri.path_and_query() { + // If `base_uri` has path, remove any trailing space and join. + // `PathAndQuery` always starts with a slash. + let base_path = pandq.path().trim_end_matches('/'); + builder = builder.path_and_query(format!("{}{}", base_path, request_pandq)); + } else { + builder = builder.path_and_query(request_pandq.as_str()); + } + builder.build().expect("valid URI") } #[cfg(test)] mod tests { #[test] fn normal_host() { - let minikube_host = "https://192.168.1.65:8443"; - let cluster_url = url::Url::parse(minikube_host).unwrap(); - let apipath: http::Uri = "/api/v1/nodes?hi=yes".parse().unwrap(); + let base_uri = http::Uri::from_static("https://192.168.1.65:8443"); + let apipath = http::Uri::from_static("/api/v1/nodes?hi=yes"); let pandq = apipath.path_and_query().expect("could pandq apipath"); - let final_url = super::finalize_url(&cluster_url, &pandq); assert_eq!( - final_url.as_str(), + super::finalize_url(&base_uri, &pandq), "https://192.168.1.65:8443/api/v1/nodes?hi=yes" ); } @@ -38,19 +45,12 @@ mod tests { #[test] fn rancher_host() { // in rancher, kubernetes server names are not hostnames, but a host with a path: - let rancher_host = "https://hostname/foo/bar"; - let cluster_url = url::Url::parse(rancher_host).unwrap(); - assert_eq!(cluster_url.host_str().unwrap(), "hostname"); - assert_eq!(cluster_url.path(), "/foo/bar"); - // we must be careful when using Url::join on our http::Uri result - // as a straight two Uri::join would trim away rancher's initial path - // case in point (discards original path): - assert_eq!(cluster_url.join("/api/v1/nodes").unwrap().path(), "/api/v1/nodes"); - - let apipath: http::Uri = "/api/v1/nodes?hi=yes".parse().unwrap(); - let pandq = apipath.path_and_query().expect("could pandq apipath"); - - let final_url = super::finalize_url(&cluster_url, &pandq); - assert_eq!(final_url.as_str(), "https://hostname/foo/bar/api/v1/nodes?hi=yes"); + let base_uri = http::Uri::from_static("https://example.com/foo/bar"); + let api_path = http::Uri::from_static("/api/v1/nodes?hi=yes"); + let pandq = api_path.path_and_query().unwrap(); + assert_eq!( + super::finalize_url(&base_uri, &pandq), + "https://example.com/foo/bar/api/v1/nodes?hi=yes" + ); } } From 7eb86973266f3f94c81f3700bfc6fb160d764f06 Mon Sep 17 00:00:00 2001 From: kazk Date: Wed, 26 May 2021 20:37:43 -0700 Subject: [PATCH 08/36] Add `SetBaseUriLayer` to set base URI of requests --- examples/custom_client.rs | 3 +- kube/src/client/mod.rs | 4 +- kube/src/lib.rs | 5 +- kube/src/service/base_uri.rs | 109 +++++++++++++++++++++++++++++++++++ kube/src/service/mod.rs | 6 +- kube/src/service/url.rs | 56 ------------------ 6 files changed, 117 insertions(+), 66 deletions(-) create mode 100644 kube/src/service/base_uri.rs delete mode 100644 kube/src/service/url.rs diff --git a/examples/custom_client.rs b/examples/custom_client.rs index 2af5037c5..bcfecb810 100644 --- a/examples/custom_client.rs +++ b/examples/custom_client.rs @@ -11,6 +11,7 @@ use tower::ServiceBuilder; use kube::{ api::{Api, DeleteParams, ListParams, PostParams, ResourceExt, WatchEvent}, + service::SetBaseUriLayer, Client, Config, }; @@ -22,7 +23,7 @@ async fn main() -> anyhow::Result<()> { let config = Config::infer().await?; let cluster_url = config.cluster_url.clone(); let common = ServiceBuilder::new() - .map_request(move |r| kube::set_cluster_url(r, &cluster_url)) + .layer(SetBaseUriLayer::new(cluster_url)) .into_inner(); let mut http = HttpConnector::new(); http.enforce_http(false); diff --git a/kube/src/client/mod.rs b/kube/src/client/mod.rs index b02383154..90c436a6c 100644 --- a/kube/src/client/mod.rs +++ b/kube/src/client/mod.rs @@ -33,7 +33,7 @@ use crate::service::{accept_compressed, maybe_decompress}; use crate::{ api::WatchEvent, error::{ConfigError, ErrorResponse}, - service::{set_cluster_url, set_default_headers, AuthLayer, Authentication, LogRequest}, + service::{set_default_headers, AuthLayer, Authentication, LogRequest, SetBaseUriLayer}, Config, Error, Result, }; @@ -411,7 +411,7 @@ impl TryFrom for Client { }; let common = ServiceBuilder::new() - .map_request(move |r| set_cluster_url(r, &cluster_url)) + .layer(SetBaseUriLayer::new(cluster_url)) .map_request(move |r| set_default_headers(r, default_headers.clone())) .into_inner(); diff --git a/kube/src/lib.rs b/kube/src/lib.rs index 0b87446a6..7a23bfa8b 100644 --- a/kube/src/lib.rs +++ b/kube/src/lib.rs @@ -107,10 +107,7 @@ cfg_client! { pub mod api; pub mod discovery; pub mod client; - pub(crate) mod service; - // Export this for examples for now. - #[doc(hidden)] - pub use service::set_cluster_url; + pub mod service; #[doc(inline)] pub use api::Api; diff --git a/kube/src/service/base_uri.rs b/kube/src/service/base_uri.rs new file mode 100644 index 000000000..545ff8c51 --- /dev/null +++ b/kube/src/service/base_uri.rs @@ -0,0 +1,109 @@ +//! Set base URI of requests. +use http::{uri, Request}; +use tower::{Layer, Service}; + +/// Layer that applies [`SetBaseUri`] which makes all requests relative to the base URI. +/// +/// Path in `base_uri` is preseved. +#[derive(Debug, Clone)] +pub struct SetBaseUriLayer { + base_uri: http::Uri, +} + +impl SetBaseUriLayer { + /// Set base URI of requests. + pub fn new(base_uri: http::Uri) -> Self { + Self { base_uri } + } +} + +impl Layer for SetBaseUriLayer { + type Service = SetBaseUri; + + fn layer(&self, inner: S) -> Self::Service { + SetBaseUri { + base_uri: self.base_uri.clone(), + inner, + } + } +} + +/// Middleware that sets base URI so that all requests are relative to it. +#[derive(Debug, Clone)] +pub struct SetBaseUri { + base_uri: http::Uri, + inner: S, +} + +impl Service> for SetBaseUri +where + S: Service>, +{ + type Error = S::Error; + type Future = S::Future; + type Response = S::Response; + + fn poll_ready(&mut self, cx: &mut std::task::Context<'_>) -> std::task::Poll> { + self.inner.poll_ready(cx) + } + + fn call(&mut self, req: Request) -> Self::Future { + let (mut parts, body) = req.into_parts(); + let req_pandq = parts.uri.path_and_query(); + parts.uri = set_base_uri(&self.base_uri, req_pandq); + self.inner.call(Request::from_parts(parts, body)) + } +} + +// Join base URI and Path+Query, preserving any path in the base. +fn set_base_uri(base_uri: &http::Uri, req_pandq: Option<&uri::PathAndQuery>) -> http::Uri { + let mut builder = uri::Builder::new(); + if let Some(scheme) = base_uri.scheme() { + builder = builder.scheme(scheme.as_str()); + } + if let Some(authority) = base_uri.authority() { + builder = builder.authority(authority.as_str()); + } + + if let Some(pandq) = base_uri.path_and_query() { + builder = if let Some(req_pandq) = req_pandq { + // Remove any trailing slashes and join. + // `PathAndQuery` always starts with a slash. + let base_path = pandq.path().trim_end_matches('/'); + builder.path_and_query(format!("{}{}", base_path, req_pandq)) + } else { + builder.path_and_query(pandq.as_str()) + }; + } else if let Some(req_pandq) = req_pandq { + builder = builder.path_and_query(req_pandq.as_str()); + } + + // Joining a valid Uri and valid PathAndQuery should result in a valid Uri. + builder.build().expect("Valid Uri") +} + +#[cfg(test)] +mod tests { + #[test] + fn normal_host() { + let base_uri = http::Uri::from_static("https://192.168.1.65:8443"); + let apipath = http::Uri::from_static("/api/v1/nodes?hi=yes"); + let pandq = apipath.path_and_query(); + assert_eq!( + super::set_base_uri(&base_uri, pandq), + "https://192.168.1.65:8443/api/v1/nodes?hi=yes" + ); + } + + #[test] + fn rancher_host() { + // in rancher, kubernetes server names are not hostnames, but a host with a path: + let base_uri = http::Uri::from_static("https://example.com/foo/bar"); + let api_path = http::Uri::from_static("/api/v1/nodes?hi=yes"); + let pandq = api_path.path_and_query(); + assert_eq!( + super::set_base_uri(&base_uri, pandq), + "https://example.com/foo/bar/api/v1/nodes?hi=yes" + ); + } +} diff --git a/kube/src/service/mod.rs b/kube/src/service/mod.rs index 71747b8dd..d878beef4 100644 --- a/kube/src/service/mod.rs +++ b/kube/src/service/mod.rs @@ -1,16 +1,16 @@ -//! Abstracts the connection to Kubernetes API server. +//! Middleware for customizing client. mod auth; +mod base_uri; #[cfg(feature = "gzip")] mod compression; mod headers; mod log; -mod url; #[cfg(feature = "gzip")] pub(crate) use self::compression::{accept_compressed, maybe_decompress}; -pub use self::url::set_cluster_url; pub(crate) use self::{ auth::{AuthLayer, Authentication}, headers::set_default_headers, log::LogRequest, }; +pub use base_uri::{SetBaseUri, SetBaseUriLayer}; diff --git a/kube/src/service/url.rs b/kube/src/service/url.rs deleted file mode 100644 index a48b845b5..000000000 --- a/kube/src/service/url.rs +++ /dev/null @@ -1,56 +0,0 @@ -use http::{uri, Request}; -use hyper::Body; - -/// Set cluster URL. -pub fn set_cluster_url(req: Request, base_uri: &http::Uri) -> Request { - let (mut parts, body) = req.into_parts(); - let request_pandq = parts.uri.path_and_query().expect("nonempty path+query"); - parts.uri = finalize_url(base_uri, request_pandq); - Request::from_parts(parts, body) -} - -// Join base URI and Path+Query, preserving any path in the base. -fn finalize_url(base_uri: &http::Uri, request_pandq: &uri::PathAndQuery) -> http::Uri { - let mut builder = uri::Builder::new(); - if let Some(scheme) = base_uri.scheme() { - builder = builder.scheme(scheme.as_str()); - } - if let Some(authority) = base_uri.authority() { - builder = builder.authority(authority.as_str()); - } - if let Some(pandq) = base_uri.path_and_query() { - // If `base_uri` has path, remove any trailing space and join. - // `PathAndQuery` always starts with a slash. - let base_path = pandq.path().trim_end_matches('/'); - builder = builder.path_and_query(format!("{}{}", base_path, request_pandq)); - } else { - builder = builder.path_and_query(request_pandq.as_str()); - } - builder.build().expect("valid URI") -} - -#[cfg(test)] -mod tests { - #[test] - fn normal_host() { - let base_uri = http::Uri::from_static("https://192.168.1.65:8443"); - let apipath = http::Uri::from_static("/api/v1/nodes?hi=yes"); - let pandq = apipath.path_and_query().expect("could pandq apipath"); - assert_eq!( - super::finalize_url(&base_uri, &pandq), - "https://192.168.1.65:8443/api/v1/nodes?hi=yes" - ); - } - - #[test] - fn rancher_host() { - // in rancher, kubernetes server names are not hostnames, but a host with a path: - let base_uri = http::Uri::from_static("https://example.com/foo/bar"); - let api_path = http::Uri::from_static("/api/v1/nodes?hi=yes"); - let pandq = api_path.path_and_query().unwrap(); - assert_eq!( - super::finalize_url(&base_uri, &pandq), - "https://example.com/foo/bar/api/v1/nodes?hi=yes" - ); - } -} From 50634fdf238c41005ae24f655977f738cee806c5 Mon Sep 17 00:00:00 2001 From: kazk Date: Tue, 1 Jun 2021 00:32:24 -0700 Subject: [PATCH 09/36] Remove hyper::Body dependency from services --- kube/Cargo.toml | 1 + kube/src/service/auth/layer.rs | 42 ++++++++++++++++----------------- kube/src/service/compression.rs | 7 +++--- kube/src/service/headers.rs | 3 +-- kube/src/service/log.rs | 10 ++++---- 5 files changed, 30 insertions(+), 33 deletions(-) diff --git a/kube/Cargo.toml b/kube/Cargo.toml index 14fb0473e..976e53305 100644 --- a/kube/Cargo.toml +++ b/kube/Cargo.toml @@ -44,6 +44,7 @@ serde = { version = "1.0.118", features = ["derive"] } serde_json = "1.0.61" serde_yaml = { version = "0.8.17", optional = true } http = "0.2.2" +http-body = "0.4.2" either = { version = "1.6.1", optional = true } thiserror = "1.0.23" futures = { version = "0.3.8", optional = true } diff --git a/kube/src/service/auth/layer.rs b/kube/src/service/auth/layer.rs index e0fd8220b..07a7b07d6 100644 --- a/kube/src/service/auth/layer.rs +++ b/kube/src/service/auth/layer.rs @@ -4,8 +4,7 @@ use std::{ }; use futures::{ready, Future}; -use http::{header::AUTHORIZATION, Request}; -use hyper::Body; +use http::{header::AUTHORIZATION, Request, Response}; use pin_project::pin_project; use tower::{layer::Layer, BoxError, Service}; @@ -23,10 +22,7 @@ impl AuthLayer { } } -impl Layer for AuthLayer -where - S: Service>, -{ +impl Layer for AuthLayer { type Service = AuthService; fn layer(&self, service: S) -> Self::Service { @@ -37,28 +33,27 @@ where } } -pub struct AuthService -where - S: Service>, -{ +pub struct AuthService { auth: RefreshableToken, service: S, } -impl Service> for AuthService +impl Service> for AuthService where - S: Service> + Clone, + S: Service, Response = Response> + Clone, S::Error: Into, + ReqB: http_body::Body + Send + Unpin + 'static, + ResB: http_body::Body, { type Error = BoxError; - type Future = AuthFuture; + type Future = AuthFuture; type Response = S::Response; fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll> { self.service.poll_ready(cx).map_err(Into::into) } - fn call(&mut self, mut req: Request) -> Self::Future { + fn call(&mut self, mut req: Request) -> Self::Future { // Comment from `AsyncFilter` // > In case the inner service has state that's driven to readiness and // > not tracked by clones (such as `Buffer`), pass the version we have @@ -91,22 +86,24 @@ enum State { Response(#[pin] G), } -type RequestFuture = Pin, BoxError>> + Send>>; +type RequestFuture = Pin, BoxError>> + Send>>; #[pin_project] -pub struct AuthFuture +pub struct AuthFuture where - S: Service>, + S: Service>, + B: http_body::Body, { #[pin] - state: State, + state: State, S::Future>, service: S, } -impl Future for AuthFuture +impl Future for AuthFuture where - S: Service>, + S: Service>, S::Error: Into, + B: http_body::Body, { type Output = Result; @@ -141,7 +138,7 @@ mod tests { use hyper::Body; use tokio::sync::Mutex; use tokio_test::assert_ready_ok; - use tower_test::mock; + use tower_test::{mock, mock::Handle}; use crate::{config::AuthInfo, error::ConfigError, Error}; @@ -149,7 +146,8 @@ mod tests { async fn valid_token() { const TOKEN: &str = "test"; let auth = test_token(TOKEN.into()); - let (mut service, handle) = mock::spawn_layer(AuthLayer::new(auth)); + let (mut service, handle): (_, Handle, Response>) = + mock::spawn_layer(AuthLayer::new(auth)); let spawned = tokio::spawn(async move { // Receive the requests and respond diff --git a/kube/src/service/compression.rs b/kube/src/service/compression.rs index e560e9874..9e08f5852 100644 --- a/kube/src/service/compression.rs +++ b/kube/src/service/compression.rs @@ -9,14 +9,13 @@ use http::{ header::{Entry, HeaderValue, ACCEPT_ENCODING, CONTENT_ENCODING, CONTENT_LENGTH, RANGE}, Request, Response, }; -use hyper::Body; use tokio_util::io::{ReaderStream, StreamReader}; /// Set `Accept-Encoding: gzip` if not already set. /// Note that Kubernetes doesn't compress the response by default yet. /// It's behind `APIResponseCompression` feature gate which is in `Beta` since `1.16`. /// See https://kubernetes.io/docs/reference/command-line-tools-reference/feature-gates/ -pub fn accept_compressed(mut req: Request) -> Request { +pub fn accept_compressed(mut req: Request) -> Request { if !req.headers().contains_key(RANGE) { if let Entry::Vacant(entry) = req.headers_mut().entry(ACCEPT_ENCODING) { entry.insert(HeaderValue::from_static("gzip")); @@ -26,7 +25,7 @@ pub fn accept_compressed(mut req: Request) -> Request { } /// Transparently decompresses compressed response. -pub fn maybe_decompress(res: Response) -> Response { +pub fn maybe_decompress(res: Response) -> Response { let (mut parts, body) = res.into_parts(); if let Entry::Occupied(entry) = parts.headers.entry(CONTENT_ENCODING) { if entry.get().as_bytes() != b"gzip" { @@ -38,7 +37,7 @@ pub fn maybe_decompress(res: Response) -> Response { let stream = ReaderStream::new(GzipDecoder::new(StreamReader::new( body.map_err(|e| IoError::new(IoErrorKind::Other, e)), ))); - Response::from_parts(parts, Body::wrap_stream(stream)) + Response::from_parts(parts, hyper::Body::wrap_stream(stream)) } else { Response::from_parts(parts, body) } diff --git a/kube/src/service/headers.rs b/kube/src/service/headers.rs index fed03c0ee..c39c6127c 100644 --- a/kube/src/service/headers.rs +++ b/kube/src/service/headers.rs @@ -1,9 +1,8 @@ use http::{header::HeaderMap, Request}; -use hyper::Body; // TODO Let users use this easily, deprecate `headers` config, and remove from default. /// Set default headers. -pub fn set_default_headers(req: Request, mut headers: HeaderMap) -> Request { +pub fn set_default_headers(req: Request, mut headers: HeaderMap) -> Request { let (mut parts, body) = req.into_parts(); headers.extend(parts.headers.into_iter()); parts.headers = headers; diff --git a/kube/src/service/log.rs b/kube/src/service/log.rs index c9acbb7aa..8981fa469 100644 --- a/kube/src/service/log.rs +++ b/kube/src/service/log.rs @@ -1,7 +1,6 @@ use std::task::{Context, Poll}; use http::Request; -use hyper::Body; use tower::Service; // `Clone` so that it can be composed with `AuthLayer`. @@ -26,9 +25,10 @@ where } } -impl Service> for LogRequest +impl Service> for LogRequest where - S: Service> + Clone, + S: Service> + Clone, + B: http_body::Body, { type Error = S::Error; type Future = S::Future; @@ -38,8 +38,8 @@ where self.service.poll_ready(cx) } - fn call(&mut self, req: Request) -> Self::Future { - tracing::debug!("{} {} {:?}", req.method(), req.uri(), req.body()); + fn call(&mut self, req: Request) -> Self::Future { + tracing::debug!("{} {}", req.method(), req.uri()); self.service.call(req) } } From 0d9e8d09d00cf76015b4e9a341b306b03516668a Mon Sep 17 00:00:00 2001 From: kazk Date: Tue, 1 Jun 2021 01:59:02 -0700 Subject: [PATCH 10/36] Relax Service's response body type Allow using more from the Tower ecosystem. --- examples/Cargo.toml | 1 + examples/custom_client.rs | 20 +++++++++++++++++++- kube/Cargo.toml | 5 +++-- kube/src/client/body.rs | 32 ++++++++++++++++++++++++++++++++ kube/src/client/mod.rs | 25 +++++++++++++++++++------ 5 files changed, 74 insertions(+), 9 deletions(-) create mode 100644 kube/src/client/body.rs diff --git a/examples/Cargo.toml b/examples/Cargo.toml index fdd00f5ac..09344c05c 100644 --- a/examples/Cargo.toml +++ b/examples/Cargo.toml @@ -50,6 +50,7 @@ warp = { version = "0.3", features = ["tls"] } http = "0.2.3" json-patch = "0.2.6" tower = { version = "0.4.6" } +tower-http = { version = "0.1.0", features = ["trace", "decompression-gzip"] } hyper = { version = "0.14.2", features = ["client", "http1", "stream", "tcp"] } [[example]] diff --git a/examples/custom_client.rs b/examples/custom_client.rs index bcfecb810..ac39b2020 100644 --- a/examples/custom_client.rs +++ b/examples/custom_client.rs @@ -9,6 +9,13 @@ use serde_json::json; use tokio_native_tls::TlsConnector; use tower::ServiceBuilder; +use tower_http::{ + decompression::DecompressionLayer, + trace::{DefaultMakeSpan, DefaultOnRequest, DefaultOnResponse, TraceLayer}, + LatencyUnit, +}; +use tracing::Level; + use kube::{ api::{Api, DeleteParams, ListParams, PostParams, ResourceExt, WatchEvent}, service::SetBaseUriLayer, @@ -17,13 +24,24 @@ use kube::{ #[tokio::main] async fn main() -> anyhow::Result<()> { - std::env::set_var("RUST_LOG", "info,kube=debug"); + std::env::set_var("RUST_LOG", "info,kube=debug,tower_http=trace"); env_logger::init(); let config = Config::infer().await?; let cluster_url = config.cluster_url.clone(); let common = ServiceBuilder::new() .layer(SetBaseUriLayer::new(cluster_url)) + .layer(DecompressionLayer::new()) + .layer( + TraceLayer::new_for_http() + .make_span_with(DefaultMakeSpan::new().include_headers(true)) + .on_request(DefaultOnRequest::new().level(Level::INFO)) + .on_response( + DefaultOnResponse::new() + .level(Level::INFO) + .latency_unit(LatencyUnit::Micros), + ), + ) .into_inner(); let mut http = HttpConnector::new(); http.enforce_http(false); diff --git a/kube/Cargo.toml b/kube/Cargo.toml index 976e53305..9b338bd52 100644 --- a/kube/Cargo.toml +++ b/kube/Cargo.toml @@ -22,7 +22,7 @@ rustls-tls = ["rustls", "rustls-pemfile", "hyper-rustls", "webpki"] ws = ["client", "tokio-tungstenite", "rand", "kube-core/ws"] oauth = ["client", "tame-oauth"] gzip = ["client", "async-compression"] -client = ["config", "__non_core", "hyper", "tower", "hyper-timeout", "pin-project", "chrono", "jsonpath_lib", "bytes", "futures", "tokio", "tokio-util", "either"] +client = ["config", "__non_core", "hyper", "tower", "tower-http", "hyper-timeout", "pin-project", "chrono", "jsonpath_lib", "bytes", "futures", "tokio", "tokio-util", "either"] jsonpatch = ["kube-core/jsonpatch"] admission = ["kube-core/admission"] derive = ["kube-derive"] @@ -60,11 +60,12 @@ kube-derive = { path = "../kube-derive", version = "^0.55.0", optional = true } kube-core = { path = "../kube-core", version = "^0.55.0"} jsonpath_lib = { version = "0.3.0", optional = true } tokio-util = { version = "0.6.0", optional = true, features = ["io", "codec"] } -hyper = { version = "0.14.2", optional = true, features = ["client", "http1", "stream", "tcp"] } +hyper = { version = "0.14.8", optional = true, features = ["client", "http1", "stream", "tcp"] } hyper-tls = { version = "0.5.0", optional = true } hyper-rustls = { version = "0.22.1", optional = true } tokio-tungstenite = { version = "0.14.0", optional = true } tower = { version = "0.4.6", optional = true, features = ["buffer", "util"] } +tower-http = { version = "0.1.0", optional = true, features = ["map-response-body"] } async-compression = { version = "0.3.7", features = ["gzip", "tokio"], optional = true } hyper-timeout = {version = "0.4.1", optional = true } tame-oauth = { version = "0.4.7", features = ["gcp"], optional = true } diff --git a/kube/src/client/body.rs b/kube/src/client/body.rs new file mode 100644 index 000000000..d0bf509b8 --- /dev/null +++ b/kube/src/client/body.rs @@ -0,0 +1,32 @@ +use std::{ + pin::Pin, + task::{Context, Poll}, +}; + +use futures::stream::Stream; +use http_body::Body; +use pin_project::pin_project; + +// Wrap `http_body::Body` to implement `Stream`. +#[pin_project] +pub struct IntoStream { + #[pin] + body: B, +} + +impl IntoStream { + pub(crate) fn new(body: B) -> Self { + Self { body } + } +} + +impl Stream for IntoStream +where + B: Body, +{ + type Item = Result; + + fn poll_next(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { + self.project().body.poll_data(cx) + } +} diff --git a/kube/src/client/mod.rs b/kube/src/client/mod.rs index 90c436a6c..6118288ed 100644 --- a/kube/src/client/mod.rs +++ b/kube/src/client/mod.rs @@ -26,7 +26,8 @@ use tokio_util::{ codec::{FramedRead, LinesCodec, LinesCodecError}, io::StreamReader, }; -use tower::{buffer::Buffer, util::BoxService, BoxError, Service, ServiceBuilder, ServiceExt}; +use tower::{buffer::Buffer, util::BoxService, BoxError, Layer, Service, ServiceBuilder, ServiceExt}; +use tower_http::map_response_body::MapResponseBodyLayer; #[cfg(feature = "gzip")] use crate::service::{accept_compressed, maybe_decompress}; @@ -37,6 +38,8 @@ use crate::{ Config, Error, Result, }; +mod body; + // Binary subprotocol v4. See `Client::connect`. #[cfg(feature = "ws")] const WS_PROTOCOL: &str = "v4.channel.k8s.io"; @@ -59,22 +62,32 @@ impl Client { /// Create and initialize a [`Client`] using the given `Service`. /// /// Use [`Client::try_from`](Self::try_from) to create with a [`Config`]. - pub fn new>(service: S) -> Self + pub fn new(service: S) -> Self where - S: Service, Response = Response, Error = E> + Send + 'static, + S: Service, Response = Response> + Send + 'static, S::Future: Send + 'static, + S::Error: Into, + B: http_body::Body + Send + 'static, + B::Error: std::error::Error + Send + Sync + 'static, { Self::new_with_default_ns(service, "default") } /// Create and initialize a [`Client`] using the given `Service` and the default namespace. - fn new_with_default_ns, T: Into>(service: S, default_ns: T) -> Self + fn new_with_default_ns>(service: S, default_ns: T) -> Self where - S: Service, Response = Response, Error = E> + Send + 'static, + S: Service, Response = Response> + Send + 'static, S::Future: Send + 'static, + S::Error: Into, + B: http_body::Body + Send + 'static, + B::Error: std::error::Error + Send + Sync + 'static, { + // Transform response body to `hyper::Body` and use type erased error to avoid type parameters. + let service = MapResponseBodyLayer::new(|b| hyper::Body::wrap_stream(body::IntoStream::new(b))) + .layer(service) + .map_err(|e| e.into()); Self { - inner: Buffer::new(BoxService::new(service.map_err(|e| e.into())), 1024), + inner: Buffer::new(BoxService::new(service), 1024), default_ns: default_ns.into(), } } From 6649d975ea9bcb8a208552cb9e6ca67e99922e2d Mon Sep 17 00:00:00 2001 From: kazk Date: Tue, 1 Jun 2021 02:05:56 -0700 Subject: [PATCH 11/36] Replace custom decompression module with `DecompressionLayer` --- kube/Cargo.toml | 3 +-- kube/src/client/mod.rs | 5 +--- kube/src/service/compression.rs | 44 --------------------------------- kube/src/service/mod.rs | 3 --- 4 files changed, 2 insertions(+), 53 deletions(-) delete mode 100644 kube/src/service/compression.rs diff --git a/kube/Cargo.toml b/kube/Cargo.toml index 9b338bd52..dafca43ae 100644 --- a/kube/Cargo.toml +++ b/kube/Cargo.toml @@ -21,7 +21,7 @@ native-tls = ["openssl", "hyper-tls", "tokio-native-tls"] rustls-tls = ["rustls", "rustls-pemfile", "hyper-rustls", "webpki"] ws = ["client", "tokio-tungstenite", "rand", "kube-core/ws"] oauth = ["client", "tame-oauth"] -gzip = ["client", "async-compression"] +gzip = ["client", "tower-http/decompression-gzip"] client = ["config", "__non_core", "hyper", "tower", "tower-http", "hyper-timeout", "pin-project", "chrono", "jsonpath_lib", "bytes", "futures", "tokio", "tokio-util", "either"] jsonpatch = ["kube-core/jsonpatch"] admission = ["kube-core/admission"] @@ -66,7 +66,6 @@ hyper-rustls = { version = "0.22.1", optional = true } tokio-tungstenite = { version = "0.14.0", optional = true } tower = { version = "0.4.6", optional = true, features = ["buffer", "util"] } tower-http = { version = "0.1.0", optional = true, features = ["map-response-body"] } -async-compression = { version = "0.3.7", features = ["gzip", "tokio"], optional = true } hyper-timeout = {version = "0.4.1", optional = true } tame-oauth = { version = "0.4.7", features = ["gcp"], optional = true } pin-project = { version = "1.0.4", optional = true } diff --git a/kube/src/client/mod.rs b/kube/src/client/mod.rs index 6118288ed..24de5ede8 100644 --- a/kube/src/client/mod.rs +++ b/kube/src/client/mod.rs @@ -29,8 +29,6 @@ use tokio_util::{ use tower::{buffer::Buffer, util::BoxService, BoxError, Layer, Service, ServiceBuilder, ServiceExt}; use tower_http::map_response_body::MapResponseBodyLayer; -#[cfg(feature = "gzip")] -use crate::service::{accept_compressed, maybe_decompress}; use crate::{ api::WatchEvent, error::{ConfigError, ErrorResponse}, @@ -431,8 +429,7 @@ impl TryFrom for Client { #[cfg(feature = "gzip")] let common = ServiceBuilder::new() .layer(common) - .map_request(accept_compressed) - .map_response(maybe_decompress) + .layer(tower_http::decompression::DecompressionLayer::new()) .into_inner(); let mut connector = HttpConnector::new(); diff --git a/kube/src/service/compression.rs b/kube/src/service/compression.rs deleted file mode 100644 index 9e08f5852..000000000 --- a/kube/src/service/compression.rs +++ /dev/null @@ -1,44 +0,0 @@ -// Couldn't use [Decompression layer](https://github.com/tower-rs/tower-http/pull/41) from tower-http -// because it changes the response body type and supporting that requires adding type parameter to `Client`. - -use std::io::{Error as IoError, ErrorKind as IoErrorKind}; - -use async_compression::tokio::bufread::GzipDecoder; -use futures::TryStreamExt; -use http::{ - header::{Entry, HeaderValue, ACCEPT_ENCODING, CONTENT_ENCODING, CONTENT_LENGTH, RANGE}, - Request, Response, -}; -use tokio_util::io::{ReaderStream, StreamReader}; - -/// Set `Accept-Encoding: gzip` if not already set. -/// Note that Kubernetes doesn't compress the response by default yet. -/// It's behind `APIResponseCompression` feature gate which is in `Beta` since `1.16`. -/// See https://kubernetes.io/docs/reference/command-line-tools-reference/feature-gates/ -pub fn accept_compressed(mut req: Request) -> Request { - if !req.headers().contains_key(RANGE) { - if let Entry::Vacant(entry) = req.headers_mut().entry(ACCEPT_ENCODING) { - entry.insert(HeaderValue::from_static("gzip")); - } - } - req -} - -/// Transparently decompresses compressed response. -pub fn maybe_decompress(res: Response) -> Response { - let (mut parts, body) = res.into_parts(); - if let Entry::Occupied(entry) = parts.headers.entry(CONTENT_ENCODING) { - if entry.get().as_bytes() != b"gzip" { - return Response::from_parts(parts, body); - } - - entry.remove(); - parts.headers.remove(CONTENT_LENGTH); - let stream = ReaderStream::new(GzipDecoder::new(StreamReader::new( - body.map_err(|e| IoError::new(IoErrorKind::Other, e)), - ))); - Response::from_parts(parts, hyper::Body::wrap_stream(stream)) - } else { - Response::from_parts(parts, body) - } -} diff --git a/kube/src/service/mod.rs b/kube/src/service/mod.rs index d878beef4..8e25bae95 100644 --- a/kube/src/service/mod.rs +++ b/kube/src/service/mod.rs @@ -2,12 +2,9 @@ mod auth; mod base_uri; -#[cfg(feature = "gzip")] mod compression; mod headers; mod log; -#[cfg(feature = "gzip")] -pub(crate) use self::compression::{accept_compressed, maybe_decompress}; pub(crate) use self::{ auth::{AuthLayer, Authentication}, headers::set_default_headers, From 992231dbc7c7ce8f0ffa83b0bb571576ee2d389f Mon Sep 17 00:00:00 2001 From: kazk Date: Tue, 1 Jun 2021 02:15:24 -0700 Subject: [PATCH 12/36] Replace `LogRequest` layer with `TraceLayer` Keeping this simple for now by default, but it's fully customizable. --- examples/pod_api.rs | 2 +- kube/Cargo.toml | 2 +- kube/src/client/mod.rs | 19 ++++++++++++++--- kube/src/service/log.rs | 45 ----------------------------------------- kube/src/service/mod.rs | 2 -- 5 files changed, 18 insertions(+), 52 deletions(-) delete mode 100644 kube/src/service/log.rs diff --git a/examples/pod_api.rs b/examples/pod_api.rs index c3c48ce3c..b915dd5c9 100644 --- a/examples/pod_api.rs +++ b/examples/pod_api.rs @@ -10,7 +10,7 @@ use kube::{ #[tokio::main] async fn main() -> anyhow::Result<()> { - std::env::set_var("RUST_LOG", "info,kube=debug"); + std::env::set_var("RUST_LOG", "info,kube=debug,tower_http=debug"); env_logger::init(); let client = Client::try_default().await?; let namespace = std::env::var("NAMESPACE").unwrap_or("default".into()); diff --git a/kube/Cargo.toml b/kube/Cargo.toml index dafca43ae..fede18b1c 100644 --- a/kube/Cargo.toml +++ b/kube/Cargo.toml @@ -65,7 +65,7 @@ hyper-tls = { version = "0.5.0", optional = true } hyper-rustls = { version = "0.22.1", optional = true } tokio-tungstenite = { version = "0.14.0", optional = true } tower = { version = "0.4.6", optional = true, features = ["buffer", "util"] } -tower-http = { version = "0.1.0", optional = true, features = ["map-response-body"] } +tower-http = { version = "0.1.0", optional = true, features = ["map-response-body", "trace"] } hyper-timeout = {version = "0.4.1", optional = true } tame-oauth = { version = "0.4.7", features = ["gcp"], optional = true } pin-project = { version = "1.0.4", optional = true } diff --git a/kube/src/client/mod.rs b/kube/src/client/mod.rs index 24de5ede8..f73e7154b 100644 --- a/kube/src/client/mod.rs +++ b/kube/src/client/mod.rs @@ -27,12 +27,17 @@ use tokio_util::{ io::StreamReader, }; use tower::{buffer::Buffer, util::BoxService, BoxError, Layer, Service, ServiceBuilder, ServiceExt}; -use tower_http::map_response_body::MapResponseBodyLayer; +use tower_http::{ + map_response_body::MapResponseBodyLayer, + trace::{DefaultOnRequest, DefaultOnResponse, TraceLayer}, + LatencyUnit, +}; +use tracing::Level; use crate::{ api::WatchEvent, error::{ConfigError, ErrorResponse}, - service::{set_default_headers, AuthLayer, Authentication, LogRequest, SetBaseUriLayer}, + service::{set_default_headers, AuthLayer, Authentication, SetBaseUriLayer}, Config, Error, Result, }; @@ -464,7 +469,15 @@ impl TryFrom for Client { let inner = ServiceBuilder::new() .layer(common) .option_layer(maybe_auth) - .layer(tower::layer::layer_fn(LogRequest::new)) + .layer( + TraceLayer::new_for_http() + .on_request(DefaultOnRequest::new().level(Level::DEBUG)) + .on_response( + DefaultOnResponse::new() + .level(Level::DEBUG) + .latency_unit(LatencyUnit::Millis), + ), + ) .service(client); Ok(Self::new_with_default_ns(inner, default_ns)) } diff --git a/kube/src/service/log.rs b/kube/src/service/log.rs deleted file mode 100644 index 8981fa469..000000000 --- a/kube/src/service/log.rs +++ /dev/null @@ -1,45 +0,0 @@ -use std::task::{Context, Poll}; - -use http::Request; -use tower::Service; - -// `Clone` so that it can be composed with `AuthLayer`. -/// Example service to log complete request before sending. -/// Can be used to support better logging of API calls. -/// https://github.com/clux/kube-rs/issues/26 -#[derive(Clone)] -pub struct LogRequest -where - S: Clone, -{ - service: S, -} - -impl LogRequest -where - S: Clone, -{ - /// Create `LogRequest` service wrapping `service`. - pub fn new(service: S) -> Self { - Self { service } - } -} - -impl Service> for LogRequest -where - S: Service> + Clone, - B: http_body::Body, -{ - type Error = S::Error; - type Future = S::Future; - type Response = S::Response; - - fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll> { - self.service.poll_ready(cx) - } - - fn call(&mut self, req: Request) -> Self::Future { - tracing::debug!("{} {}", req.method(), req.uri()); - self.service.call(req) - } -} diff --git a/kube/src/service/mod.rs b/kube/src/service/mod.rs index 8e25bae95..087796e4c 100644 --- a/kube/src/service/mod.rs +++ b/kube/src/service/mod.rs @@ -3,11 +3,9 @@ mod auth; mod base_uri; mod headers; -mod log; pub(crate) use self::{ auth::{AuthLayer, Authentication}, headers::set_default_headers, - log::LogRequest, }; pub use base_uri::{SetBaseUri, SetBaseUriLayer}; From 977ba6365050569609220491a9c60b9b8f00f155 Mon Sep 17 00:00:00 2001 From: kazk Date: Tue, 1 Jun 2021 17:33:12 -0700 Subject: [PATCH 13/36] Split custom client examples --- examples/Cargo.toml | 8 +++ examples/custom_client.rs | 70 ++++++------------- examples/custom_client_tls.rs | 104 +++++++++++++++++++++++++++++ examples/custom_client_trace.rs | 115 ++++++++++++++++++++++++++++++++ 4 files changed, 246 insertions(+), 51 deletions(-) create mode 100644 examples/custom_client_tls.rs create mode 100644 examples/custom_client_trace.rs diff --git a/examples/Cargo.toml b/examples/Cargo.toml index 09344c05c..7007ebb9f 100644 --- a/examples/Cargo.toml +++ b/examples/Cargo.toml @@ -172,3 +172,11 @@ path = "admission_controller.rs" [[example]] name = "custom_client" path = "custom_client.rs" + +[[example]] +name = "custom_client_tls" +path = "custom_client_tls.rs" + +[[example]] +name = "custom_client_trace" +path = "custom_client_trace.rs" diff --git a/examples/custom_client.rs b/examples/custom_client.rs index ac39b2020..53473a49c 100644 --- a/examples/custom_client.rs +++ b/examples/custom_client.rs @@ -1,21 +1,11 @@ -// Run with `cargo run --example custom_client --no-default-features --features native-tls,rustls-tls` -#[macro_use] extern crate log; +// Minimal custom client example. use futures::{StreamExt, TryStreamExt}; use hyper::client::HttpConnector; -use hyper_rustls::HttpsConnector as RustlsHttpsConnector; use hyper_tls::HttpsConnector; use k8s_openapi::api::core::v1::Pod; use serde_json::json; -use tokio_native_tls::TlsConnector; use tower::ServiceBuilder; -use tower_http::{ - decompression::DecompressionLayer, - trace::{DefaultMakeSpan, DefaultOnRequest, DefaultOnResponse, TraceLayer}, - LatencyUnit, -}; -use tracing::Level; - use kube::{ api::{Api, DeleteParams, ListParams, PostParams, ResourceExt, WatchEvent}, service::SetBaseUriLayer, @@ -24,44 +14,22 @@ use kube::{ #[tokio::main] async fn main() -> anyhow::Result<()> { - std::env::set_var("RUST_LOG", "info,kube=debug,tower_http=trace"); - env_logger::init(); + std::env::set_var("RUST_LOG", "info,kube=debug"); + tracing_subscriber::fmt::init(); let config = Config::infer().await?; - let cluster_url = config.cluster_url.clone(); - let common = ServiceBuilder::new() - .layer(SetBaseUriLayer::new(cluster_url)) - .layer(DecompressionLayer::new()) - .layer( - TraceLayer::new_for_http() - .make_span_with(DefaultMakeSpan::new().include_headers(true)) - .on_request(DefaultOnRequest::new().level(Level::INFO)) - .on_response( - DefaultOnResponse::new() - .level(Level::INFO) - .latency_unit(LatencyUnit::Micros), - ), - ) - .into_inner(); - let mut http = HttpConnector::new(); - http.enforce_http(false); - - // Pick TLS at runtime - let use_rustls = std::env::var("USE_RUSTLS").map(|s| s == "1").unwrap_or(false); - let client = if use_rustls { - let https = - RustlsHttpsConnector::from((http, std::sync::Arc::new(config.rustls_tls_client_config()?))); - let inner = ServiceBuilder::new() - .layer(common) - .service(hyper::Client::builder().build(https)); - Client::new(inner) - } else { - let https = HttpsConnector::from((http, TlsConnector::from(config.native_tls_connector()?))); - let inner = ServiceBuilder::new() - .layer(common) - .service(hyper::Client::builder().build(https)); - Client::new(inner) + // Create HttpsConnector using `native_tls::TlsConnector` based on `Config`. + let https = { + let tls = tokio_native_tls::TlsConnector::from(config.native_tls_connector()?); + let mut http = HttpConnector::new(); + http.enforce_http(false); + HttpsConnector::from((http, tls)) }; + let client = Client::new( + ServiceBuilder::new() + .layer(SetBaseUriLayer::new(config.cluster_url)) + .service(hyper::Client::builder().build(https)), + ); // Manage pods let pods: Api = Api::namespaced(client, "default"); @@ -78,7 +46,7 @@ async fn main() -> anyhow::Result<()> { Ok(o) => { let name = o.name(); assert_eq!(p.name(), name); - info!("Created {}", name); + tracing::info!("Created {}", name); std::thread::sleep(std::time::Duration::from_millis(5_000)); } Err(kube::Error::Api(ae)) => assert_eq!(ae.code, 409), // if you skipped delete, for instance @@ -92,14 +60,14 @@ async fn main() -> anyhow::Result<()> { let mut stream = pods.watch(&lp, "0").await?.boxed(); while let Some(status) = stream.try_next().await? { match status { - WatchEvent::Added(o) => info!("Added {}", o.name()), + WatchEvent::Added(o) => tracing::info!("Added {}", o.name()), WatchEvent::Modified(o) => { let s = o.status.as_ref().expect("status exists on pod"); let phase = s.phase.clone().unwrap_or_default(); - info!("Modified: {} with phase: {}", o.name(), phase); + tracing::info!("Modified: {} with phase: {}", o.name(), phase); } - WatchEvent::Deleted(o) => info!("Deleted {}", o.name()), - WatchEvent::Error(e) => error!("Error {}", e), + WatchEvent::Deleted(o) => tracing::info!("Deleted {}", o.name()), + WatchEvent::Error(e) => tracing::error!("Error {}", e), _ => {} } } diff --git a/examples/custom_client_tls.rs b/examples/custom_client_tls.rs new file mode 100644 index 000000000..b1fe45a75 --- /dev/null +++ b/examples/custom_client_tls.rs @@ -0,0 +1,104 @@ +// Custom client supporting both native-tls and rustls-tls +// Run with `USE_RUSTLS=1` to pick rustls. +use std::sync::Arc; + +use futures::{StreamExt, TryStreamExt}; +use hyper::client::HttpConnector; +use k8s_openapi::api::core::v1::Pod; +use serde_json::json; +use tower::ServiceBuilder; + +use kube::{ + api::{Api, DeleteParams, ListParams, PostParams, ResourceExt, WatchEvent}, + service::SetBaseUriLayer, + Client, Config, +}; + +#[tokio::main] +async fn main() -> anyhow::Result<()> { + std::env::set_var("RUST_LOG", "info,kube=debug"); + tracing_subscriber::fmt::init(); + + let config = Config::infer().await?; + + // Pick TLS at runtime + let use_rustls = std::env::var("USE_RUSTLS").map(|s| s == "1").unwrap_or(false); + let client = if use_rustls { + let https = { + let rustls_config = Arc::new(config.rustls_tls_client_config()?); + let mut http = HttpConnector::new(); + http.enforce_http(false); + hyper_rustls::HttpsConnector::from((http, rustls_config)) + }; + Client::new( + ServiceBuilder::new() + .layer(SetBaseUriLayer::new(config.cluster_url)) + .service(hyper::Client::builder().build(https)), + ) + } else { + let https = { + let tls = tokio_native_tls::TlsConnector::from(config.native_tls_connector()?); + let mut http = HttpConnector::new(); + http.enforce_http(false); + hyper_tls::HttpsConnector::from((http, tls)) + }; + Client::new( + ServiceBuilder::new() + .layer(SetBaseUriLayer::new(config.cluster_url)) + .service(hyper::Client::builder().build(https)), + ) + }; + + // Manage pods + let pods: Api = Api::namespaced(client, "default"); + // Create pod + let p: Pod = serde_json::from_value(json!({ + "apiVersion": "v1", + "kind": "Pod", + "metadata": { "name": "example" }, + "spec": { "containers": [{ "name": "example", "image": "alpine" }] } + }))?; + + let pp = PostParams::default(); + match pods.create(&pp, &p).await { + Ok(o) => { + let name = o.name(); + assert_eq!(p.name(), name); + tracing::info!("Created {}", name); + std::thread::sleep(std::time::Duration::from_millis(5_000)); + } + Err(kube::Error::Api(ae)) => assert_eq!(ae.code, 409), // if you skipped delete, for instance + Err(e) => return Err(e.into()), + } + + // Watch it phase for a few seconds + let lp = ListParams::default() + .fields(&format!("metadata.name={}", "example")) + .timeout(10); + let mut stream = pods.watch(&lp, "0").await?.boxed(); + while let Some(status) = stream.try_next().await? { + match status { + WatchEvent::Added(o) => tracing::info!("Added {}", o.name()), + WatchEvent::Modified(o) => { + let s = o.status.as_ref().expect("status exists on pod"); + let phase = s.phase.clone().unwrap_or_default(); + tracing::info!("Modified: {} with phase: {}", o.name(), phase); + } + WatchEvent::Deleted(o) => tracing::info!("Deleted {}", o.name()), + WatchEvent::Error(e) => tracing::error!("Error {}", e), + _ => {} + } + } + + if let Some(spec) = &pods.get("example").await?.spec { + assert_eq!(spec.containers[0].name, "example"); + } + + pods.delete("example", &DeleteParams::default()) + .await? + .map_left(|pdel| { + assert_eq!(pdel.name(), "example"); + }); + + Ok(()) +} diff --git a/examples/custom_client_trace.rs b/examples/custom_client_trace.rs new file mode 100644 index 000000000..537264921 --- /dev/null +++ b/examples/custom_client_trace.rs @@ -0,0 +1,115 @@ +// Custom client example with TraceLayer. +use std::time::Duration; + +use futures::{StreamExt, TryStreamExt}; +use http::{Request, Response}; +use hyper::{client::HttpConnector, Body}; +use hyper_tls::HttpsConnector; +use k8s_openapi::api::core::v1::Pod; +use serde_json::json; +use tower::ServiceBuilder; +use tower_http::{decompression::DecompressionLayer, trace::TraceLayer}; +use tracing::Span; + +use kube::{ + api::{Api, DeleteParams, ListParams, PostParams, ResourceExt, WatchEvent}, + service::SetBaseUriLayer, + Client, Config, +}; + +#[tokio::main] +async fn main() -> anyhow::Result<()> { + std::env::set_var("RUST_LOG", "info,kube=debug,custom_client_trace=debug"); + tracing_subscriber::fmt::init(); + + let config = Config::infer().await?; + // Create HttpsConnector using `native_tls::TlsConnector` based on `Config`. + let https = { + let tls = tokio_native_tls::TlsConnector::from(config.native_tls_connector()?); + let mut http = HttpConnector::new(); + http.enforce_http(false); + HttpsConnector::from((http, tls)) + }; + let client = Client::new( + ServiceBuilder::new() + .layer(SetBaseUriLayer::new(config.cluster_url)) + // Add `DecompressionLayer` to make request headers interesting. + .layer(DecompressionLayer::new()) + .layer( + TraceLayer::new_for_http() + .make_span_with(|request: &Request| { + tracing::debug_span!( + "HTTP", + otel.name = %format!("HTTP {}", request.method()), + http.method = %request.method(), + http.url = %request.uri(), + http.status_code = tracing::field::Empty, + ) + }) + .on_request(|request: &Request, _span: &Span| { + tracing::debug!("payload: {:?} headers: {:?}", request.body(), request.headers()) + }) + .on_response(|response: &Response, latency: Duration, span: &Span| { + span.record( + "http.status_code", + &tracing::field::display(response.status().as_u16()), + ); + tracing::debug!("finished in {}ms", latency.as_millis()) + }), + ) + .service(hyper::Client::builder().build(https)), + ); + + // Manage pods + let pods: Api = Api::namespaced(client, "default"); + // Create pod + let p: Pod = serde_json::from_value(json!({ + "apiVersion": "v1", + "kind": "Pod", + "metadata": { "name": "example" }, + "spec": { "containers": [{ "name": "example", "image": "alpine" }] } + }))?; + + let pp = PostParams::default(); + match pods.create(&pp, &p).await { + Ok(o) => { + let name = o.name(); + assert_eq!(p.name(), name); + tracing::info!("Created {}", name); + std::thread::sleep(std::time::Duration::from_millis(5_000)); + } + Err(kube::Error::Api(ae)) => assert_eq!(ae.code, 409), // if you skipped delete, for instance + Err(e) => return Err(e.into()), + } + + // Watch it phase for a few seconds + let lp = ListParams::default() + .fields(&format!("metadata.name={}", "example")) + .timeout(10); + let mut stream = pods.watch(&lp, "0").await?.boxed(); + while let Some(status) = stream.try_next().await? { + match status { + WatchEvent::Added(o) => tracing::info!("Added {}", o.name()), + WatchEvent::Modified(o) => { + let s = o.status.as_ref().expect("status exists on pod"); + let phase = s.phase.clone().unwrap_or_default(); + tracing::info!("Modified: {} with phase: {}", o.name(), phase); + } + WatchEvent::Deleted(o) => tracing::info!("Deleted {}", o.name()), + WatchEvent::Error(e) => tracing::error!("Error {}", e), + _ => {} + } + } + + if let Some(spec) = &pods.get("example").await?.spec { + assert_eq!(spec.containers[0].name, "example"); + } + + pods.delete("example", &DeleteParams::default()) + .await? + .map_left(|pdel| { + assert_eq!(pdel.name(), "example"); + }); + + Ok(()) +} From 36d39fd3a37ccb33b58e1747926d1711385d81d5 Mon Sep 17 00:00:00 2001 From: kazk Date: Tue, 1 Jun 2021 21:28:46 -0700 Subject: [PATCH 14/36] Switch to custom tracing with callbacks `kube=debug` can be used as before. --- examples/pod_api.rs | 27 ++++++++++----------- kube/src/client/mod.rs | 54 ++++++++++++++++++++++++++++++++++-------- 2 files changed, 57 insertions(+), 24 deletions(-) diff --git a/examples/pod_api.rs b/examples/pod_api.rs index b915dd5c9..e796e375c 100644 --- a/examples/pod_api.rs +++ b/examples/pod_api.rs @@ -1,4 +1,3 @@ -#[macro_use] extern crate log; use futures::{StreamExt, TryStreamExt}; use k8s_openapi::api::core::v1::Pod; use serde_json::json; @@ -10,8 +9,8 @@ use kube::{ #[tokio::main] async fn main() -> anyhow::Result<()> { - std::env::set_var("RUST_LOG", "info,kube=debug,tower_http=debug"); - env_logger::init(); + std::env::set_var("RUST_LOG", "info,kube=debug"); + tracing_subscriber::fmt::init(); let client = Client::try_default().await?; let namespace = std::env::var("NAMESPACE").unwrap_or("default".into()); @@ -19,7 +18,7 @@ async fn main() -> anyhow::Result<()> { let pods: Api = Api::namespaced(client, &namespace); // Create Pod blog - info!("Creating Pod instance blog"); + tracing::info!("Creating Pod instance blog"); let p: Pod = serde_json::from_value(json!({ "apiVersion": "v1", "kind": "Pod", @@ -37,7 +36,7 @@ async fn main() -> anyhow::Result<()> { Ok(o) => { let name = o.name(); assert_eq!(p.name(), name); - info!("Created {}", name); + tracing::info!("Created {}", name); // wait for it.. std::thread::sleep(std::time::Duration::from_millis(5_000)); } @@ -52,28 +51,28 @@ async fn main() -> anyhow::Result<()> { let mut stream = pods.watch(&lp, "0").await?.boxed(); while let Some(status) = stream.try_next().await? { match status { - WatchEvent::Added(o) => info!("Added {}", o.name()), + WatchEvent::Added(o) => tracing::info!("Added {}", o.name()), WatchEvent::Modified(o) => { let s = o.status.as_ref().expect("status exists on pod"); let phase = s.phase.clone().unwrap_or_default(); - info!("Modified: {} with phase: {}", o.name(), phase); + tracing::info!("Modified: {} with phase: {}", o.name(), phase); } - WatchEvent::Deleted(o) => info!("Deleted {}", o.name()), - WatchEvent::Error(e) => error!("Error {}", e), + WatchEvent::Deleted(o) => tracing::info!("Deleted {}", o.name()), + WatchEvent::Error(e) => tracing::error!("Error {}", e), _ => {} } } // Verify we can get it - info!("Get Pod blog"); + tracing::info!("Get Pod blog"); let p1cpy = pods.get("blog").await?; if let Some(spec) = &p1cpy.spec { - info!("Got blog pod with containers: {:?}", spec.containers); + tracing::info!("Got blog pod with containers: {:?}", spec.containers); assert_eq!(spec.containers[0].name, "blog"); } // Replace its spec - info!("Patch Pod blog"); + tracing::info!("Patch Pod blog"); let patch = json!({ "metadata": { "resourceVersion": p1cpy.resource_version(), @@ -88,14 +87,14 @@ async fn main() -> anyhow::Result<()> { let lp = ListParams::default().fields(&format!("metadata.name={}", "blog")); // only want results for our pod for p in pods.list(&lp).await? { - info!("Found Pod: {}", p.name()); + tracing::info!("Found Pod: {}", p.name()); } // Delete it let dp = DeleteParams::default(); pods.delete("blog", &dp).await?.map_left(|pdel| { assert_eq!(pdel.name(), "blog"); - info!("Deleting blog pod started: {:?}", pdel); + tracing::info!("Deleting blog pod started: {:?}", pdel); }); Ok(()) diff --git a/kube/src/client/mod.rs b/kube/src/client/mod.rs index f73e7154b..f371b01cb 100644 --- a/kube/src/client/mod.rs +++ b/kube/src/client/mod.rs @@ -28,11 +28,8 @@ use tokio_util::{ }; use tower::{buffer::Buffer, util::BoxService, BoxError, Layer, Service, ServiceBuilder, ServiceExt}; use tower_http::{ - map_response_body::MapResponseBodyLayer, - trace::{DefaultOnRequest, DefaultOnResponse, TraceLayer}, - LatencyUnit, + classify::ServerErrorsFailureClass, map_response_body::MapResponseBodyLayer, trace::TraceLayer, }; -use tracing::Level; use crate::{ api::WatchEvent, @@ -401,6 +398,11 @@ impl TryFrom for Client { /// Convert [`Config`] into a [`Client`] fn try_from(config: Config) -> Result { + use std::time::Duration; + + use http::header::HeaderMap; + use tracing::Span; + let cluster_url = config.cluster_url.clone(); let mut default_headers = config.headers.clone(); let timeout = config.timeout; @@ -470,13 +472,45 @@ impl TryFrom for Client { .layer(common) .option_layer(maybe_auth) .layer( + // TODO Add OTEL attributes? https://github.com/clux/kube-rs/issues/457 + // - https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/trace/semantic_conventions/http.md + // - https://docs.rs/tracing-opentelemetry/0.13.0/tracing_opentelemetry/#semantic-conventions + // - https://docs.rs/tracing-opentelemetry/0.13.0/tracing_opentelemetry/#special-fields TraceLayer::new_for_http() - .on_request(DefaultOnRequest::new().level(Level::DEBUG)) - .on_response( - DefaultOnResponse::new() - .level(Level::DEBUG) - .latency_unit(LatencyUnit::Millis), - ), + .make_span_with(|req: &Request| { + tracing::debug_span!( + "HTTP", + http.method = %req.method(), + http.uri = %req.uri(), + http.status_code = tracing::field::Empty, + ) + }) + .on_request(|_req: &Request, _span: &Span| tracing::debug!("requesting")) + .on_response(|res: &Response, latency: Duration, span: &Span| { + span.record("http.status_code", &res.status().as_u16()); + tracing::debug!("finished in {}ms", latency.as_millis()) + }) + // Explicitly disable `on_body_chunk`. The default does nothing. + .on_body_chunk(()) + .on_eos(|_: Option<&HeaderMap>, stream_duration: Duration, _span: &Span| { + tracing::debug!("stream ended after {}ms", stream_duration.as_millis()) + }) + .on_failure(|ec: ServerErrorsFailureClass, latency: Duration, span: &Span| { + // Called when + // - Calling the inner service errored + // - Polling `Body` errored + // - the response was classified as failure (5xx) + // - End of stream was classified as failure + match ec { + ServerErrorsFailureClass::StatusCode(status) => { + span.record("http.status_code", &status.as_u16()); + tracing::error!("failed in {}ms {}", latency.as_millis(), status) + } + ServerErrorsFailureClass::Error(err) => { + tracing::error!("failed in {}ms {}", latency.as_millis(), err) + } + } + }), ) .service(client); Ok(Self::new_with_default_ns(inner, default_ns)) From ef0eae0e3dc175a2201a40486a81116a378e2dba Mon Sep 17 00:00:00 2001 From: kazk Date: Wed, 2 Jun 2021 17:25:39 -0700 Subject: [PATCH 15/36] Improve default tracing - Fix `http.url` - Remove unnecessary duration logs - Add `otel.kind` and `otel.status_code` --- examples/custom_client_trace.rs | 15 ++++++++++----- kube/src/client/mod.rs | 34 +++++++++++++++++++-------------- 2 files changed, 30 insertions(+), 19 deletions(-) diff --git a/examples/custom_client_trace.rs b/examples/custom_client_trace.rs index 537264921..1b09a69c9 100644 --- a/examples/custom_client_trace.rs +++ b/examples/custom_client_trace.rs @@ -36,24 +36,29 @@ async fn main() -> anyhow::Result<()> { // Add `DecompressionLayer` to make request headers interesting. .layer(DecompressionLayer::new()) .layer( + // Attribute names follow [Semantic Conventions]. + // [Semantic Conventions]: https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/trace/semantic_conventions/http.md#http-client TraceLayer::new_for_http() .make_span_with(|request: &Request| { tracing::debug_span!( "HTTP", - otel.name = %format!("HTTP {}", request.method()), http.method = %request.method(), http.url = %request.uri(), http.status_code = tracing::field::Empty, + otel.name = %format!("HTTP {}", request.method()), + otel.kind = "client", + otel.status_code = tracing::field::Empty, ) }) .on_request(|request: &Request, _span: &Span| { tracing::debug!("payload: {:?} headers: {:?}", request.body(), request.headers()) }) .on_response(|response: &Response, latency: Duration, span: &Span| { - span.record( - "http.status_code", - &tracing::field::display(response.status().as_u16()), - ); + let status = response.status(); + span.record("http.status_code", &status.as_u16()); + if status.is_client_error() || status.is_server_error() { + span.record("otel.status_code", &"ERROR"); + } tracing::debug!("finished in {}ms", latency.as_millis()) }), ) diff --git a/kube/src/client/mod.rs b/kube/src/client/mod.rs index f371b01cb..246222482 100644 --- a/kube/src/client/mod.rs +++ b/kube/src/client/mod.rs @@ -472,42 +472,48 @@ impl TryFrom for Client { .layer(common) .option_layer(maybe_auth) .layer( - // TODO Add OTEL attributes? https://github.com/clux/kube-rs/issues/457 - // - https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/trace/semantic_conventions/http.md - // - https://docs.rs/tracing-opentelemetry/0.13.0/tracing_opentelemetry/#semantic-conventions - // - https://docs.rs/tracing-opentelemetry/0.13.0/tracing_opentelemetry/#special-fields + // Attribute names follow [Semantic Conventions]. + // [Semantic Conventions]: https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/trace/semantic_conventions/http.md TraceLayer::new_for_http() .make_span_with(|req: &Request| { tracing::debug_span!( "HTTP", http.method = %req.method(), - http.uri = %req.uri(), + http.url = %req.uri(), http.status_code = tracing::field::Empty, + otel.kind = "client", + otel.status_code = tracing::field::Empty, ) }) - .on_request(|_req: &Request, _span: &Span| tracing::debug!("requesting")) - .on_response(|res: &Response, latency: Duration, span: &Span| { - span.record("http.status_code", &res.status().as_u16()); - tracing::debug!("finished in {}ms", latency.as_millis()) + .on_request(|_req: &Request, _span: &Span| { + tracing::debug!("requesting"); + }) + .on_response(|res: &Response, _latency: Duration, span: &Span| { + let status = res.status(); + span.record("http.status_code", &status.as_u16()); + if status.is_client_error() || status.is_server_error() { + span.record("otel.status_code", &"ERROR"); + } }) // Explicitly disable `on_body_chunk`. The default does nothing. .on_body_chunk(()) - .on_eos(|_: Option<&HeaderMap>, stream_duration: Duration, _span: &Span| { - tracing::debug!("stream ended after {}ms", stream_duration.as_millis()) + .on_eos(|_: Option<&HeaderMap>, _duration: Duration, _span: &Span| { + tracing::debug!("stream closed"); }) - .on_failure(|ec: ServerErrorsFailureClass, latency: Duration, span: &Span| { + .on_failure(|ec: ServerErrorsFailureClass, _latency: Duration, span: &Span| { // Called when // - Calling the inner service errored // - Polling `Body` errored // - the response was classified as failure (5xx) // - End of stream was classified as failure + span.record("otel.status_code", &"ERROR"); match ec { ServerErrorsFailureClass::StatusCode(status) => { span.record("http.status_code", &status.as_u16()); - tracing::error!("failed in {}ms {}", latency.as_millis(), status) + tracing::error!("failed with status {}", status) } ServerErrorsFailureClass::Error(err) => { - tracing::error!("failed in {}ms {}", latency.as_millis(), err) + tracing::error!("failed with error {}", err) } } }), From a3a1dc770ff1218637975d94fb75f97523a84c53 Mon Sep 17 00:00:00 2001 From: kazk Date: Wed, 2 Jun 2021 20:39:46 -0700 Subject: [PATCH 16/36] Simplify custom client examples --- examples/custom_client.rs | 58 +++----------------------------- examples/custom_client_tls.rs | 59 ++++----------------------------- examples/custom_client_trace.rs | 58 +++----------------------------- 3 files changed, 16 insertions(+), 159 deletions(-) diff --git a/examples/custom_client.rs b/examples/custom_client.rs index 53473a49c..1a6637384 100644 --- a/examples/custom_client.rs +++ b/examples/custom_client.rs @@ -1,13 +1,11 @@ // Minimal custom client example. -use futures::{StreamExt, TryStreamExt}; use hyper::client::HttpConnector; use hyper_tls::HttpsConnector; -use k8s_openapi::api::core::v1::Pod; -use serde_json::json; +use k8s_openapi::api::core::v1::ConfigMap; use tower::ServiceBuilder; use kube::{ - api::{Api, DeleteParams, ListParams, PostParams, ResourceExt, WatchEvent}, + api::{Api, ListParams}, service::SetBaseUriLayer, Client, Config, }; @@ -31,56 +29,10 @@ async fn main() -> anyhow::Result<()> { .service(hyper::Client::builder().build(https)), ); - // Manage pods - let pods: Api = Api::namespaced(client, "default"); - // Create pod - let p: Pod = serde_json::from_value(json!({ - "apiVersion": "v1", - "kind": "Pod", - "metadata": { "name": "example" }, - "spec": { "containers": [{ "name": "example", "image": "alpine" }] } - }))?; - - let pp = PostParams::default(); - match pods.create(&pp, &p).await { - Ok(o) => { - let name = o.name(); - assert_eq!(p.name(), name); - tracing::info!("Created {}", name); - std::thread::sleep(std::time::Duration::from_millis(5_000)); - } - Err(kube::Error::Api(ae)) => assert_eq!(ae.code, 409), // if you skipped delete, for instance - Err(e) => return Err(e.into()), - } - - // Watch it phase for a few seconds - let lp = ListParams::default() - .fields(&format!("metadata.name={}", "example")) - .timeout(10); - let mut stream = pods.watch(&lp, "0").await?.boxed(); - while let Some(status) = stream.try_next().await? { - match status { - WatchEvent::Added(o) => tracing::info!("Added {}", o.name()), - WatchEvent::Modified(o) => { - let s = o.status.as_ref().expect("status exists on pod"); - let phase = s.phase.clone().unwrap_or_default(); - tracing::info!("Modified: {} with phase: {}", o.name(), phase); - } - WatchEvent::Deleted(o) => tracing::info!("Deleted {}", o.name()), - WatchEvent::Error(e) => tracing::error!("Error {}", e), - _ => {} - } + let cms: Api = Api::namespaced(client, "default"); + for cm in cms.list(&ListParams::default()).await? { + println!("{:?}", cm); } - if let Some(spec) = &pods.get("example").await?.spec { - assert_eq!(spec.containers[0].name, "example"); - } - - pods.delete("example", &DeleteParams::default()) - .await? - .map_left(|pdel| { - assert_eq!(pdel.name(), "example"); - }); - Ok(()) } diff --git a/examples/custom_client_tls.rs b/examples/custom_client_tls.rs index b1fe45a75..00813509d 100644 --- a/examples/custom_client_tls.rs +++ b/examples/custom_client_tls.rs @@ -1,15 +1,14 @@ // Custom client supporting both native-tls and rustls-tls +// Must enable `rustls-tls` feature to run this. // Run with `USE_RUSTLS=1` to pick rustls. use std::sync::Arc; -use futures::{StreamExt, TryStreamExt}; use hyper::client::HttpConnector; -use k8s_openapi::api::core::v1::Pod; -use serde_json::json; +use k8s_openapi::api::core::v1::ConfigMap; use tower::ServiceBuilder; use kube::{ - api::{Api, DeleteParams, ListParams, PostParams, ResourceExt, WatchEvent}, + api::{Api, ListParams}, service::SetBaseUriLayer, Client, Config, }; @@ -49,56 +48,10 @@ async fn main() -> anyhow::Result<()> { ) }; - // Manage pods - let pods: Api = Api::namespaced(client, "default"); - // Create pod - let p: Pod = serde_json::from_value(json!({ - "apiVersion": "v1", - "kind": "Pod", - "metadata": { "name": "example" }, - "spec": { "containers": [{ "name": "example", "image": "alpine" }] } - }))?; - - let pp = PostParams::default(); - match pods.create(&pp, &p).await { - Ok(o) => { - let name = o.name(); - assert_eq!(p.name(), name); - tracing::info!("Created {}", name); - std::thread::sleep(std::time::Duration::from_millis(5_000)); - } - Err(kube::Error::Api(ae)) => assert_eq!(ae.code, 409), // if you skipped delete, for instance - Err(e) => return Err(e.into()), - } - - // Watch it phase for a few seconds - let lp = ListParams::default() - .fields(&format!("metadata.name={}", "example")) - .timeout(10); - let mut stream = pods.watch(&lp, "0").await?.boxed(); - while let Some(status) = stream.try_next().await? { - match status { - WatchEvent::Added(o) => tracing::info!("Added {}", o.name()), - WatchEvent::Modified(o) => { - let s = o.status.as_ref().expect("status exists on pod"); - let phase = s.phase.clone().unwrap_or_default(); - tracing::info!("Modified: {} with phase: {}", o.name(), phase); - } - WatchEvent::Deleted(o) => tracing::info!("Deleted {}", o.name()), - WatchEvent::Error(e) => tracing::error!("Error {}", e), - _ => {} - } + let cms: Api = Api::namespaced(client, "default"); + for cm in cms.list(&ListParams::default()).await? { + println!("{:?}", cm); } - if let Some(spec) = &pods.get("example").await?.spec { - assert_eq!(spec.containers[0].name, "example"); - } - - pods.delete("example", &DeleteParams::default()) - .await? - .map_left(|pdel| { - assert_eq!(pdel.name(), "example"); - }); - Ok(()) } diff --git a/examples/custom_client_trace.rs b/examples/custom_client_trace.rs index 1b09a69c9..096575271 100644 --- a/examples/custom_client_trace.rs +++ b/examples/custom_client_trace.rs @@ -1,18 +1,16 @@ // Custom client example with TraceLayer. use std::time::Duration; -use futures::{StreamExt, TryStreamExt}; use http::{Request, Response}; use hyper::{client::HttpConnector, Body}; use hyper_tls::HttpsConnector; -use k8s_openapi::api::core::v1::Pod; -use serde_json::json; +use k8s_openapi::api::core::v1::ConfigMap; use tower::ServiceBuilder; use tower_http::{decompression::DecompressionLayer, trace::TraceLayer}; use tracing::Span; use kube::{ - api::{Api, DeleteParams, ListParams, PostParams, ResourceExt, WatchEvent}, + api::{Api, ListParams}, service::SetBaseUriLayer, Client, Config, }; @@ -65,56 +63,10 @@ async fn main() -> anyhow::Result<()> { .service(hyper::Client::builder().build(https)), ); - // Manage pods - let pods: Api = Api::namespaced(client, "default"); - // Create pod - let p: Pod = serde_json::from_value(json!({ - "apiVersion": "v1", - "kind": "Pod", - "metadata": { "name": "example" }, - "spec": { "containers": [{ "name": "example", "image": "alpine" }] } - }))?; - - let pp = PostParams::default(); - match pods.create(&pp, &p).await { - Ok(o) => { - let name = o.name(); - assert_eq!(p.name(), name); - tracing::info!("Created {}", name); - std::thread::sleep(std::time::Duration::from_millis(5_000)); - } - Err(kube::Error::Api(ae)) => assert_eq!(ae.code, 409), // if you skipped delete, for instance - Err(e) => return Err(e.into()), - } - - // Watch it phase for a few seconds - let lp = ListParams::default() - .fields(&format!("metadata.name={}", "example")) - .timeout(10); - let mut stream = pods.watch(&lp, "0").await?.boxed(); - while let Some(status) = stream.try_next().await? { - match status { - WatchEvent::Added(o) => tracing::info!("Added {}", o.name()), - WatchEvent::Modified(o) => { - let s = o.status.as_ref().expect("status exists on pod"); - let phase = s.phase.clone().unwrap_or_default(); - tracing::info!("Modified: {} with phase: {}", o.name(), phase); - } - WatchEvent::Deleted(o) => tracing::info!("Deleted {}", o.name()), - WatchEvent::Error(e) => tracing::error!("Error {}", e), - _ => {} - } + let cms: Api = Api::namespaced(client, "default"); + for cm in cms.list(&ListParams::default()).await? { + println!("{:?}", cm); } - if let Some(spec) = &pods.get("example").await?.spec { - assert_eq!(spec.containers[0].name, "example"); - } - - pods.delete("example", &DeleteParams::default()) - .await? - .map_left(|pdel| { - assert_eq!(pdel.name(), "example"); - }); - Ok(()) } From 7e0c49be949c709fe2d41a3c1287994dfd6a5dac Mon Sep 17 00:00:00 2001 From: kazk Date: Wed, 2 Jun 2021 20:40:03 -0700 Subject: [PATCH 17/36] Fix examples having both TLS enabled by default --- examples/Cargo.toml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/examples/Cargo.toml b/examples/Cargo.toml index 7007ebb9f..1f47ffffe 100644 --- a/examples/Cargo.toml +++ b/examples/Cargo.toml @@ -10,7 +10,7 @@ publish = false edition = "2018" [features] -default = ["native-tls", "rustls-tls", "schema", "kubederive", "ws"] +default = ["native-tls", "schema", "kubederive", "ws"] kubederive = ["kube/derive"] # by default import kube-derive with its default features schema = ["kube-derive/schema"] # crd_derive_no_schema shows how to opt out native-tls = ["kube/client", "kube/native-tls", "hyper-tls", "tokio-native-tls"] @@ -176,6 +176,7 @@ path = "custom_client.rs" [[example]] name = "custom_client_tls" path = "custom_client_tls.rs" +required-features = ["native-tls", "rustls-tls"] [[example]] name = "custom_client_trace" From 891cef13f9dc16a7fe96812517d33cb70d2a4bbf Mon Sep 17 00:00:00 2001 From: kazk Date: Wed, 2 Jun 2021 21:11:26 -0700 Subject: [PATCH 18/36] Clean up body transformer --- kube/src/client/body.rs | 11 +++++++++++ kube/src/client/mod.rs | 4 +++- 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/kube/src/client/body.rs b/kube/src/client/body.rs index d0bf509b8..aec7116f1 100644 --- a/kube/src/client/body.rs +++ b/kube/src/client/body.rs @@ -30,3 +30,14 @@ where self.project().body.poll_data(cx) } } + +pub trait BodyStreamExt: Body { + fn into_stream(self) -> IntoStream + where + Self: Sized, + { + IntoStream::new(self) + } +} + +impl BodyStreamExt for T where T: Body {} diff --git a/kube/src/client/mod.rs b/kube/src/client/mod.rs index 246222482..647a7eee6 100644 --- a/kube/src/client/mod.rs +++ b/kube/src/client/mod.rs @@ -39,6 +39,8 @@ use crate::{ }; mod body; +// Add `into_stream()` to `http::Body` +use body::BodyStreamExt; // Binary subprotocol v4. See `Client::connect`. #[cfg(feature = "ws")] @@ -83,7 +85,7 @@ impl Client { B::Error: std::error::Error + Send + Sync + 'static, { // Transform response body to `hyper::Body` and use type erased error to avoid type parameters. - let service = MapResponseBodyLayer::new(|b| hyper::Body::wrap_stream(body::IntoStream::new(b))) + let service = MapResponseBodyLayer::new(|b: B| Body::wrap_stream(b.into_stream())) .layer(service) .map_err(|e| e.into()); Self { From 055eceb966586eaef8e0a485ca877f9117103466 Mon Sep 17 00:00:00 2001 From: kazk Date: Wed, 2 Jun 2021 22:03:01 -0700 Subject: [PATCH 19/36] Replace `set_default_headers` with `SetHeadersLayer` --- kube/src/client/mod.rs | 4 +-- kube/src/service/headers.rs | 58 ++++++++++++++++++++++++++++++++----- kube/src/service/mod.rs | 2 +- 3 files changed, 54 insertions(+), 10 deletions(-) diff --git a/kube/src/client/mod.rs b/kube/src/client/mod.rs index 647a7eee6..f87670d99 100644 --- a/kube/src/client/mod.rs +++ b/kube/src/client/mod.rs @@ -34,7 +34,7 @@ use tower_http::{ use crate::{ api::WatchEvent, error::{ConfigError, ErrorResponse}, - service::{set_default_headers, AuthLayer, Authentication, SetBaseUriLayer}, + service::{AuthLayer, Authentication, SetBaseUriLayer, SetHeadersLayer}, Config, Error, Result, }; @@ -432,7 +432,7 @@ impl TryFrom for Client { let common = ServiceBuilder::new() .layer(SetBaseUriLayer::new(cluster_url)) - .map_request(move |r| set_default_headers(r, default_headers.clone())) + .layer(SetHeadersLayer::new(default_headers)) .into_inner(); #[cfg(feature = "gzip")] diff --git a/kube/src/service/headers.rs b/kube/src/service/headers.rs index c39c6127c..dd624002d 100644 --- a/kube/src/service/headers.rs +++ b/kube/src/service/headers.rs @@ -1,10 +1,54 @@ use http::{header::HeaderMap, Request}; +use tower::{Layer, Service}; -// TODO Let users use this easily, deprecate `headers` config, and remove from default. -/// Set default headers. -pub fn set_default_headers(req: Request, mut headers: HeaderMap) -> Request { - let (mut parts, body) = req.into_parts(); - headers.extend(parts.headers.into_iter()); - parts.headers = headers; - Request::from_parts(parts, body) +/// Layer that applies [`SetHeaders`] which sets the provided headers to each request. +#[derive(Debug, Clone)] +pub struct SetHeadersLayer { + headers: HeaderMap, +} + +impl SetHeadersLayer { + /// Create a new [`SetHeadersLayer`]. + pub fn new(headers: HeaderMap) -> Self { + Self { headers } + } +} + +impl Layer for SetHeadersLayer { + type Service = SetHeaders; + + fn layer(&self, inner: S) -> Self::Service { + SetHeaders { + headers: self.headers.clone(), + inner, + } + } +} + +/// Middleware that set headers. +#[derive(Debug, Clone)] +pub struct SetHeaders { + headers: HeaderMap, + inner: S, +} + +impl Service> for SetHeaders +where + S: Service>, +{ + type Error = S::Error; + type Future = S::Future; + type Response = S::Response; + + fn poll_ready(&mut self, cx: &mut std::task::Context<'_>) -> std::task::Poll> { + self.inner.poll_ready(cx) + } + + fn call(&mut self, req: Request) -> Self::Future { + let (mut parts, body) = req.into_parts(); + let mut headers = self.headers.clone(); + headers.extend(parts.headers.into_iter()); + parts.headers = headers; + self.inner.call(Request::from_parts(parts, body)) + } } diff --git a/kube/src/service/mod.rs b/kube/src/service/mod.rs index 087796e4c..f9883276d 100644 --- a/kube/src/service/mod.rs +++ b/kube/src/service/mod.rs @@ -6,6 +6,6 @@ mod headers; pub(crate) use self::{ auth::{AuthLayer, Authentication}, - headers::set_default_headers, + headers::SetHeadersLayer, }; pub use base_uri::{SetBaseUri, SetBaseUriLayer}; From 25ddfe03c2318baffab7fe347169edd79f9679b1 Mon Sep 17 00:00:00 2001 From: kazk Date: Wed, 2 Jun 2021 23:15:17 -0700 Subject: [PATCH 20/36] Refactor auth with layers --- kube/src/client/mod.rs | 30 ++------- kube/src/service/auth/add_authorization.rs | 63 +++++++++++++++++++ kube/src/service/auth/mod.rs | 32 +++++++--- .../auth/{layer.rs => refreshing_token.rs} | 33 +++++----- kube/src/service/mod.rs | 5 +- 5 files changed, 109 insertions(+), 54 deletions(-) create mode 100644 kube/src/service/auth/add_authorization.rs rename kube/src/service/auth/{layer.rs => refreshing_token.rs} (87%) diff --git a/kube/src/client/mod.rs b/kube/src/client/mod.rs index f87670d99..55c2270ee 100644 --- a/kube/src/client/mod.rs +++ b/kube/src/client/mod.rs @@ -13,7 +13,7 @@ use std::convert::TryFrom; use bytes::Bytes; use either::{Either, Left, Right}; use futures::{self, Stream, StreamExt, TryStream, TryStreamExt}; -use http::{self, HeaderValue, Request, Response, StatusCode}; +use http::{self, Request, Response, StatusCode}; use hyper::{client::HttpConnector, Body}; use hyper_timeout::TimeoutConnector; use k8s_openapi::apimachinery::pkg::apis::meta::v1 as k8s_meta_v1; @@ -33,8 +33,8 @@ use tower_http::{ use crate::{ api::WatchEvent, - error::{ConfigError, ErrorResponse}, - service::{AuthLayer, Authentication, SetBaseUriLayer, SetHeadersLayer}, + error::ErrorResponse, + service::{Authentication, SetBaseUriLayer, SetHeadersLayer}, Config, Error, Result, }; @@ -406,30 +406,10 @@ impl TryFrom for Client { use tracing::Span; let cluster_url = config.cluster_url.clone(); - let mut default_headers = config.headers.clone(); + let default_headers = config.headers.clone(); let timeout = config.timeout; let default_ns = config.default_ns.clone(); - // AuthLayer is not necessary unless `RefreshableToken` - let maybe_auth = match Authentication::try_from(&config.auth_info)? { - Authentication::None => None, - Authentication::Basic(s) => { - let mut value = - HeaderValue::try_from(format!("Basic {}", &s)).map_err(ConfigError::InvalidBasicAuth)?; - value.set_sensitive(true); - default_headers.insert(http::header::AUTHORIZATION, value); - None - } - Authentication::Token(s) => { - let mut value = HeaderValue::try_from(format!("Bearer {}", &s)) - .map_err(ConfigError::InvalidBearerToken)?; - value.set_sensitive(true); - default_headers.insert(http::header::AUTHORIZATION, value); - None - } - Authentication::RefreshableToken(r) => Some(AuthLayer::new(r)), - }; - let common = ServiceBuilder::new() .layer(SetBaseUriLayer::new(cluster_url)) .layer(SetHeadersLayer::new(default_headers)) @@ -472,7 +452,7 @@ impl TryFrom for Client { let inner = ServiceBuilder::new() .layer(common) - .option_layer(maybe_auth) + .option_layer(Authentication::try_from(&config.auth_info)?.into_layer()) .layer( // Attribute names follow [Semantic Conventions]. // [Semantic Conventions]: https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/trace/semantic_conventions/http.md diff --git a/kube/src/service/auth/add_authorization.rs b/kube/src/service/auth/add_authorization.rs new file mode 100644 index 000000000..6c3e2428b --- /dev/null +++ b/kube/src/service/auth/add_authorization.rs @@ -0,0 +1,63 @@ +// Borrowing from https://github.com/tower-rs/tower-http/pull/95 +// TODO Use `tower-http`'s version once released +use std::task::{Context, Poll}; + +use http::{HeaderValue, Request}; +use tower::{layer::Layer, Service}; + +#[derive(Debug, Clone)] +pub struct AddAuthorizationLayer { + value: HeaderValue, +} + +impl AddAuthorizationLayer { + pub fn basic(username: &str, password: &str) -> Self { + let encoded = base64::encode(format!("{}:{}", username, password)); + let mut value = HeaderValue::from_str(&format!("Basic {}", encoded)).unwrap(); + value.set_sensitive(true); + Self { value } + } + + pub fn bearer(token: &str) -> Self { + let mut value = + HeaderValue::from_str(&format!("Bearer {}", token)).expect("token is not valid header"); + value.set_sensitive(true); + Self { value } + } +} + +impl Layer for AddAuthorizationLayer { + type Service = AddAuthorization; + + fn layer(&self, inner: S) -> Self::Service { + AddAuthorization { + inner, + value: self.value.clone(), + } + } +} + +#[derive(Debug, Clone)] +pub struct AddAuthorization { + inner: S, + value: HeaderValue, +} + +impl Service> for AddAuthorization +where + S: Service>, +{ + type Error = S::Error; + type Future = S::Future; + type Response = S::Response; + + fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll> { + self.inner.poll_ready(cx) + } + + fn call(&mut self, mut req: Request) -> Self::Future { + req.headers_mut() + .insert(http::header::AUTHORIZATION, self.value.clone()); + self.inner.call(req) + } +} diff --git a/kube/src/service/auth/mod.rs b/kube/src/service/auth/mod.rs index 1d70b095e..b6033d79a 100644 --- a/kube/src/service/auth/mod.rs +++ b/kube/src/service/auth/mod.rs @@ -5,6 +5,7 @@ use http::HeaderValue; use jsonpath_lib::select as jsonpath_select; use serde::{Deserialize, Serialize}; use tokio::sync::Mutex; +use tower::util::Either; use crate::{ config::{read_file_to_string, AuthInfo, AuthProviderConfig, ExecConfig}, @@ -14,18 +15,31 @@ use crate::{ #[cfg(feature = "oauth")] mod oauth; -mod layer; -pub(crate) use layer::AuthLayer; +mod add_authorization; +mod refreshing_token; +pub(crate) use add_authorization::AddAuthorizationLayer; +pub(crate) use refreshing_token::RefreshingTokenLayer; #[derive(Debug, Clone)] #[allow(clippy::large_enum_variant)] pub(crate) enum Authentication { None, - Basic(String), - Token(String), + Basic(String, String), + Bearer(String), RefreshableToken(RefreshableToken), } +impl Authentication { + pub(crate) fn into_layer(self) -> Option> { + match self { + Authentication::None => None, + Authentication::Basic(user, pass) => Some(Either::A(AddAuthorizationLayer::basic(&user, &pass))), + Authentication::Bearer(token) => Some(Either::A(AddAuthorizationLayer::bearer(&token))), + Authentication::RefreshableToken(r) => Some(Either::B(RefreshingTokenLayer::new(r))), + } + } +} + // See https://github.com/kubernetes/kubernetes/tree/master/staging/src/k8s.io/client-go/plugin/pkg/client/auth // for the list of auth-plugins supported by client-go. // We currently support the following: @@ -48,7 +62,7 @@ impl RefreshableToken { // conditions where the token expires while we are refreshing if Utc::now() + Duration::seconds(60) >= locked_data.1 { match Authentication::try_from(&locked_data.2)? { - Authentication::None | Authentication::Basic(_) | Authentication::Token(_) => { + Authentication::None | Authentication::Basic(_, _) | Authentication::Bearer(_) => { return Err(ConfigError::UnrefreshableTokenResponse).map_err(Error::from); } @@ -96,7 +110,7 @@ impl TryFrom<&AuthInfo> for Authentication { if let Some(provider) = &auth_info.auth_provider { match token_from_provider(provider)? { ProviderToken::Oidc(token) => { - return Ok(Self::Token(token)); + return Ok(Self::Bearer(token)); } ProviderToken::GcpCommand(token, Some(expiry)) => { @@ -111,7 +125,7 @@ impl TryFrom<&AuthInfo> for Authentication { } ProviderToken::GcpCommand(token, None) => { - return Ok(Self::Token(token)); + return Ok(Self::Bearer(token)); } #[cfg(feature = "oauth")] @@ -124,7 +138,7 @@ impl TryFrom<&AuthInfo> for Authentication { } if let (Some(u), Some(p)) = (&auth_info.username, &auth_info.password) { - return Ok(Authentication::Basic(base64::encode(&format!("{}:{}", u, p)))); + return Ok(Authentication::Basic(u.to_owned(), p.to_owned())); } let (raw_token, expiration) = match &auth_info.token { @@ -148,7 +162,7 @@ impl TryFrom<&AuthInfo> for Authentication { }; match (raw_token, expiration) { - (Some(token), None) => Ok(Authentication::Token(token)), + (Some(token), None) => Ok(Authentication::Bearer(token)), (Some(token), Some(expire)) => Ok(Authentication::RefreshableToken(RefreshableToken::Exec( Arc::new(Mutex::new((token, expire, auth_info.clone()))), ))), diff --git a/kube/src/service/auth/layer.rs b/kube/src/service/auth/refreshing_token.rs similarity index 87% rename from kube/src/service/auth/layer.rs rename to kube/src/service/auth/refreshing_token.rs index 07a7b07d6..ff5e204b7 100644 --- a/kube/src/service/auth/layer.rs +++ b/kube/src/service/auth/refreshing_token.rs @@ -11,34 +11,35 @@ use tower::{layer::Layer, BoxError, Service}; use super::RefreshableToken; use crate::Result; -/// `Layer` to decorate the request with `Authorization` header. -pub struct AuthLayer { - auth: RefreshableToken, +// TODO Come up with a better name +/// `Layer` to decorate the request with `Authorization` header with token refreshing automatically. +pub struct RefreshingTokenLayer { + refreshable: RefreshableToken, } -impl AuthLayer { - pub(crate) fn new(auth: RefreshableToken) -> Self { - Self { auth } +impl RefreshingTokenLayer { + pub(crate) fn new(refreshable: RefreshableToken) -> Self { + Self { refreshable } } } -impl Layer for AuthLayer { - type Service = AuthService; +impl Layer for RefreshingTokenLayer { + type Service = RefreshingToken; fn layer(&self, service: S) -> Self::Service { - AuthService { - auth: self.auth.clone(), + RefreshingToken { + refreshable: self.refreshable.clone(), service, } } } -pub struct AuthService { - auth: RefreshableToken, +pub struct RefreshingToken { + refreshable: RefreshableToken, service: S, } -impl Service> for AuthService +impl Service> for RefreshingToken where S: Service, Response = Response> + Clone, S::Error: Into, @@ -62,7 +63,7 @@ where let service = self.service.clone(); let service = std::mem::replace(&mut self.service, service); - let auth = self.auth.clone(); + let auth = self.refreshable.clone(); let request = async move { auth.to_header().await.map_err(BoxError::from).map(|value| { req.headers_mut().insert(AUTHORIZATION, value); @@ -147,7 +148,7 @@ mod tests { const TOKEN: &str = "test"; let auth = test_token(TOKEN.into()); let (mut service, handle): (_, Handle, Response>) = - mock::spawn_layer(AuthLayer::new(auth)); + mock::spawn_layer(RefreshingTokenLayer::new(auth)); let spawned = tokio::spawn(async move { // Receive the requests and respond @@ -173,7 +174,7 @@ mod tests { const TOKEN: &str = "\n"; let auth = test_token(TOKEN.into()); let (mut service, _handle) = - mock::spawn_layer::, Response, _>(AuthLayer::new(auth)); + mock::spawn_layer::, Response, _>(RefreshingTokenLayer::new(auth)); let err = service .call(Request::builder().uri("/").body(Body::empty()).unwrap()) .await diff --git a/kube/src/service/mod.rs b/kube/src/service/mod.rs index f9883276d..db7dda50f 100644 --- a/kube/src/service/mod.rs +++ b/kube/src/service/mod.rs @@ -4,8 +4,5 @@ mod auth; mod base_uri; mod headers; -pub(crate) use self::{ - auth::{AuthLayer, Authentication}, - headers::SetHeadersLayer, -}; +pub(crate) use self::{auth::Authentication, headers::SetHeadersLayer}; pub use base_uri::{SetBaseUri, SetBaseUriLayer}; From a5bb2bc88cba394df70f17dbd70f3fea73335017 Mon Sep 17 00:00:00 2001 From: kazk Date: Thu, 3 Jun 2021 02:31:00 -0700 Subject: [PATCH 21/36] Add methods to create HttpsConnector directly from Config --- examples/Cargo.toml | 7 ++----- examples/custom_client.rs | 10 +--------- examples/custom_client_tls.rs | 17 ++--------------- examples/custom_client_trace.rs | 11 ++--------- kube/src/config/tls.rs | 26 ++++++++++++++++++++++++++ 5 files changed, 33 insertions(+), 38 deletions(-) diff --git a/examples/Cargo.toml b/examples/Cargo.toml index 1f47ffffe..7c0c56d2a 100644 --- a/examples/Cargo.toml +++ b/examples/Cargo.toml @@ -13,15 +13,12 @@ edition = "2018" default = ["native-tls", "schema", "kubederive", "ws"] kubederive = ["kube/derive"] # by default import kube-derive with its default features schema = ["kube-derive/schema"] # crd_derive_no_schema shows how to opt out -native-tls = ["kube/client", "kube/native-tls", "hyper-tls", "tokio-native-tls"] -rustls-tls = ["kube/client", "kube/rustls-tls", "hyper-rustls"] +native-tls = ["kube/client", "kube/native-tls"] +rustls-tls = ["kube/client", "kube/rustls-tls"] ws = ["kube/ws"] [dependencies] tokio-util = "0.6.0" -hyper-tls = { version = "0.5.0", optional = true } -tokio-native-tls = { version = "0.3.0", optional = true } -hyper-rustls = { version = "0.22.1", optional = true } [dev-dependencies] anyhow = "1.0.37" diff --git a/examples/custom_client.rs b/examples/custom_client.rs index 1a6637384..2e85d3bfb 100644 --- a/examples/custom_client.rs +++ b/examples/custom_client.rs @@ -1,6 +1,4 @@ // Minimal custom client example. -use hyper::client::HttpConnector; -use hyper_tls::HttpsConnector; use k8s_openapi::api::core::v1::ConfigMap; use tower::ServiceBuilder; @@ -16,13 +14,7 @@ async fn main() -> anyhow::Result<()> { tracing_subscriber::fmt::init(); let config = Config::infer().await?; - // Create HttpsConnector using `native_tls::TlsConnector` based on `Config`. - let https = { - let tls = tokio_native_tls::TlsConnector::from(config.native_tls_connector()?); - let mut http = HttpConnector::new(); - http.enforce_http(false); - HttpsConnector::from((http, tls)) - }; + let https = config.native_tls_https_connector()?; let client = Client::new( ServiceBuilder::new() .layer(SetBaseUriLayer::new(config.cluster_url)) diff --git a/examples/custom_client_tls.rs b/examples/custom_client_tls.rs index 00813509d..a211a886a 100644 --- a/examples/custom_client_tls.rs +++ b/examples/custom_client_tls.rs @@ -1,9 +1,6 @@ // Custom client supporting both native-tls and rustls-tls // Must enable `rustls-tls` feature to run this. // Run with `USE_RUSTLS=1` to pick rustls. -use std::sync::Arc; - -use hyper::client::HttpConnector; use k8s_openapi::api::core::v1::ConfigMap; use tower::ServiceBuilder; @@ -23,24 +20,14 @@ async fn main() -> anyhow::Result<()> { // Pick TLS at runtime let use_rustls = std::env::var("USE_RUSTLS").map(|s| s == "1").unwrap_or(false); let client = if use_rustls { - let https = { - let rustls_config = Arc::new(config.rustls_tls_client_config()?); - let mut http = HttpConnector::new(); - http.enforce_http(false); - hyper_rustls::HttpsConnector::from((http, rustls_config)) - }; + let https = config.rustls_tls_https_connector()?; Client::new( ServiceBuilder::new() .layer(SetBaseUriLayer::new(config.cluster_url)) .service(hyper::Client::builder().build(https)), ) } else { - let https = { - let tls = tokio_native_tls::TlsConnector::from(config.native_tls_connector()?); - let mut http = HttpConnector::new(); - http.enforce_http(false); - hyper_tls::HttpsConnector::from((http, tls)) - }; + let https = config.native_tls_https_connector()?; Client::new( ServiceBuilder::new() .layer(SetBaseUriLayer::new(config.cluster_url)) diff --git a/examples/custom_client_trace.rs b/examples/custom_client_trace.rs index 096575271..1ce6bdb7c 100644 --- a/examples/custom_client_trace.rs +++ b/examples/custom_client_trace.rs @@ -2,8 +2,7 @@ use std::time::Duration; use http::{Request, Response}; -use hyper::{client::HttpConnector, Body}; -use hyper_tls::HttpsConnector; +use hyper::Body; use k8s_openapi::api::core::v1::ConfigMap; use tower::ServiceBuilder; use tower_http::{decompression::DecompressionLayer, trace::TraceLayer}; @@ -21,13 +20,7 @@ async fn main() -> anyhow::Result<()> { tracing_subscriber::fmt::init(); let config = Config::infer().await?; - // Create HttpsConnector using `native_tls::TlsConnector` based on `Config`. - let https = { - let tls = tokio_native_tls::TlsConnector::from(config.native_tls_connector()?); - let mut http = HttpConnector::new(); - http.enforce_http(false); - HttpsConnector::from((http, tls)) - }; + let https = config.native_tls_https_connector()?; let client = Client::new( ServiceBuilder::new() .layer(SetBaseUriLayer::new(config.cluster_url)) diff --git a/kube/src/config/tls.rs b/kube/src/config/tls.rs index 3a350f785..a9f7aa445 100644 --- a/kube/src/config/tls.rs +++ b/kube/src/config/tls.rs @@ -4,6 +4,7 @@ use super::Config; impl Config { /// Create `native_tls::TlsConnector` + #[cfg_attr(docsrs, doc(cfg(feature = "native-tls")))] #[cfg(feature = "native-tls")] pub fn native_tls_connector(&self) -> Result { self::native_tls::native_tls_connector( @@ -13,7 +14,20 @@ impl Config { ) } + /// Create `hyper_tls::HttpsConnector` + #[cfg_attr(docsrs, doc(cfg(all(feature = "client", feature = "native-tls"))))] + #[cfg(all(feature = "client", feature = "native-tls"))] + pub fn native_tls_https_connector( + &self, + ) -> Result> { + let tls = tokio_native_tls::TlsConnector::from(self.native_tls_connector()?); + let mut http = hyper::client::HttpConnector::new(); + http.enforce_http(false); + Ok(hyper_tls::HttpsConnector::from((http, tls))) + } + /// Create `rustls::ClientConfig` + #[cfg_attr(docsrs, doc(cfg(feature = "rustls-tls")))] #[cfg(feature = "rustls-tls")] pub fn rustls_tls_client_config(&self) -> Result { self::rustls_tls::rustls_client_config( @@ -22,6 +36,18 @@ impl Config { self.accept_invalid_certs, ) } + + /// Create `hyper_rustls::HttpsConnector` + #[cfg_attr(docsrs, doc(cfg(all(feature = "client", feature = "rustls-tls"))))] + #[cfg(all(feature = "client", feature = "rustls-tls"))] + pub fn rustls_tls_https_connector( + &self, + ) -> Result> { + let rustls_config = std::sync::Arc::new(self.rustls_tls_client_config()?); + let mut http = hyper::client::HttpConnector::new(); + http.enforce_http(false); + Ok(hyper_rustls::HttpsConnector::from((http, rustls_config))) + } } From b8ba3bc2014fab801b12cee996a18cba36e1cb30 Mon Sep 17 00:00:00 2001 From: kazk Date: Thu, 3 Jun 2021 12:02:36 -0700 Subject: [PATCH 22/36] Move layers under client --- examples/custom_client.rs | 2 +- examples/custom_client_tls.rs | 2 +- examples/custom_client_trace.rs | 2 +- .../{service => client}/auth/add_authorization.rs | 0 kube/src/{service => client}/auth/mod.rs | 0 kube/src/{service => client}/auth/oauth.rs | 0 .../{service => client}/auth/refreshing_token.rs | 0 kube/src/{service => client}/base_uri.rs | 0 kube/src/{service => client}/headers.rs | 0 kube/src/client/mod.rs | 13 +++++++------ kube/src/lib.rs | 1 - kube/src/service/mod.rs | 8 -------- 12 files changed, 10 insertions(+), 18 deletions(-) rename kube/src/{service => client}/auth/add_authorization.rs (100%) rename kube/src/{service => client}/auth/mod.rs (100%) rename kube/src/{service => client}/auth/oauth.rs (100%) rename kube/src/{service => client}/auth/refreshing_token.rs (100%) rename kube/src/{service => client}/base_uri.rs (100%) rename kube/src/{service => client}/headers.rs (100%) delete mode 100644 kube/src/service/mod.rs diff --git a/examples/custom_client.rs b/examples/custom_client.rs index 2e85d3bfb..098add9a5 100644 --- a/examples/custom_client.rs +++ b/examples/custom_client.rs @@ -4,7 +4,7 @@ use tower::ServiceBuilder; use kube::{ api::{Api, ListParams}, - service::SetBaseUriLayer, + client::SetBaseUriLayer, Client, Config, }; diff --git a/examples/custom_client_tls.rs b/examples/custom_client_tls.rs index a211a886a..8b6925aba 100644 --- a/examples/custom_client_tls.rs +++ b/examples/custom_client_tls.rs @@ -6,7 +6,7 @@ use tower::ServiceBuilder; use kube::{ api::{Api, ListParams}, - service::SetBaseUriLayer, + client::SetBaseUriLayer, Client, Config, }; diff --git a/examples/custom_client_trace.rs b/examples/custom_client_trace.rs index 1ce6bdb7c..7c1f480e6 100644 --- a/examples/custom_client_trace.rs +++ b/examples/custom_client_trace.rs @@ -10,7 +10,7 @@ use tracing::Span; use kube::{ api::{Api, ListParams}, - service::SetBaseUriLayer, + client::SetBaseUriLayer, Client, Config, }; diff --git a/kube/src/service/auth/add_authorization.rs b/kube/src/client/auth/add_authorization.rs similarity index 100% rename from kube/src/service/auth/add_authorization.rs rename to kube/src/client/auth/add_authorization.rs diff --git a/kube/src/service/auth/mod.rs b/kube/src/client/auth/mod.rs similarity index 100% rename from kube/src/service/auth/mod.rs rename to kube/src/client/auth/mod.rs diff --git a/kube/src/service/auth/oauth.rs b/kube/src/client/auth/oauth.rs similarity index 100% rename from kube/src/service/auth/oauth.rs rename to kube/src/client/auth/oauth.rs diff --git a/kube/src/service/auth/refreshing_token.rs b/kube/src/client/auth/refreshing_token.rs similarity index 100% rename from kube/src/service/auth/refreshing_token.rs rename to kube/src/client/auth/refreshing_token.rs diff --git a/kube/src/service/base_uri.rs b/kube/src/client/base_uri.rs similarity index 100% rename from kube/src/service/base_uri.rs rename to kube/src/client/base_uri.rs diff --git a/kube/src/service/headers.rs b/kube/src/client/headers.rs similarity index 100% rename from kube/src/service/headers.rs rename to kube/src/client/headers.rs diff --git a/kube/src/client/mod.rs b/kube/src/client/mod.rs index 55c2270ee..60cf09153 100644 --- a/kube/src/client/mod.rs +++ b/kube/src/client/mod.rs @@ -31,16 +31,17 @@ use tower_http::{ classify::ServerErrorsFailureClass, map_response_body::MapResponseBodyLayer, trace::TraceLayer, }; -use crate::{ - api::WatchEvent, - error::ErrorResponse, - service::{Authentication, SetBaseUriLayer, SetHeadersLayer}, - Config, Error, Result, -}; +use crate::{api::WatchEvent, error::ErrorResponse, Config, Error, Result}; +mod auth; +use auth::Authentication; +mod base_uri; +pub use base_uri::{SetBaseUri, SetBaseUriLayer}; mod body; // Add `into_stream()` to `http::Body` use body::BodyStreamExt; +mod headers; +use headers::SetHeadersLayer; // Binary subprotocol v4. See `Client::connect`. #[cfg(feature = "ws")] diff --git a/kube/src/lib.rs b/kube/src/lib.rs index 7a23bfa8b..bd0cec915 100644 --- a/kube/src/lib.rs +++ b/kube/src/lib.rs @@ -107,7 +107,6 @@ cfg_client! { pub mod api; pub mod discovery; pub mod client; - pub mod service; #[doc(inline)] pub use api::Api; diff --git a/kube/src/service/mod.rs b/kube/src/service/mod.rs deleted file mode 100644 index db7dda50f..000000000 --- a/kube/src/service/mod.rs +++ /dev/null @@ -1,8 +0,0 @@ -//! Middleware for customizing client. - -mod auth; -mod base_uri; -mod headers; - -pub(crate) use self::{auth::Authentication, headers::SetHeadersLayer}; -pub use base_uri::{SetBaseUri, SetBaseUriLayer}; From 5e7fc1cd71f4eb8c1a9fbe35a160f6165622685a Mon Sep 17 00:00:00 2001 From: kazk Date: Thu, 3 Jun 2021 13:30:01 -0700 Subject: [PATCH 23/36] Remove `_tls` suffix from rustls methods --- examples/custom_client_tls.rs | 2 +- kube/src/client/mod.rs | 2 +- kube/src/config/tls.rs | 6 +++--- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/examples/custom_client_tls.rs b/examples/custom_client_tls.rs index 8b6925aba..a4c7c8166 100644 --- a/examples/custom_client_tls.rs +++ b/examples/custom_client_tls.rs @@ -20,7 +20,7 @@ async fn main() -> anyhow::Result<()> { // Pick TLS at runtime let use_rustls = std::env::var("USE_RUSTLS").map(|s| s == "1").unwrap_or(false); let client = if use_rustls { - let https = config.rustls_tls_https_connector()?; + let https = config.rustls_https_connector()?; Client::new( ServiceBuilder::new() .layer(SetBaseUriLayer::new(config.cluster_url)) diff --git a/kube/src/client/mod.rs b/kube/src/client/mod.rs index 60cf09153..7b5f9069a 100644 --- a/kube/src/client/mod.rs +++ b/kube/src/client/mod.rs @@ -437,7 +437,7 @@ impl TryFrom for Client { #[cfg(all(not(feature = "native-tls"), feature = "rustls-tls"))] let connector = hyper_rustls::HttpsConnector::from(( connector, - std::sync::Arc::new(config.rustls_tls_client_config()?), + std::sync::Arc::new(config.rustls_client_config()?), )); let mut connector = TimeoutConnector::new(connector); diff --git a/kube/src/config/tls.rs b/kube/src/config/tls.rs index a9f7aa445..a5c4bf074 100644 --- a/kube/src/config/tls.rs +++ b/kube/src/config/tls.rs @@ -29,7 +29,7 @@ impl Config { /// Create `rustls::ClientConfig` #[cfg_attr(docsrs, doc(cfg(feature = "rustls-tls")))] #[cfg(feature = "rustls-tls")] - pub fn rustls_tls_client_config(&self) -> Result { + pub fn rustls_client_config(&self) -> Result { self::rustls_tls::rustls_client_config( self.identity_pem.as_ref(), self.root_cert.as_ref(), @@ -40,10 +40,10 @@ impl Config { /// Create `hyper_rustls::HttpsConnector` #[cfg_attr(docsrs, doc(cfg(all(feature = "client", feature = "rustls-tls"))))] #[cfg(all(feature = "client", feature = "rustls-tls"))] - pub fn rustls_tls_https_connector( + pub fn rustls_https_connector( &self, ) -> Result> { - let rustls_config = std::sync::Arc::new(self.rustls_tls_client_config()?); + let rustls_config = std::sync::Arc::new(self.rustls_client_config()?); let mut http = hyper::client::HttpConnector::new(); http.enforce_http(false); Ok(hyper_rustls::HttpsConnector::from((http, rustls_config))) From 8536b2ada3491f1d4241057264a78dad4bc144e0 Mon Sep 17 00:00:00 2001 From: kazk Date: Thu, 3 Jun 2021 21:18:19 -0700 Subject: [PATCH 24/36] Pass span name through Extensions and remove instrument in Api --- kube/src/api/core_methods.rs | 33 +++++++++++++-------------- kube/src/api/subresource.rs | 44 ++++++++++++++++++------------------ kube/src/client/mod.rs | 1 + 3 files changed, 39 insertions(+), 39 deletions(-) diff --git a/kube/src/api/core_methods.rs b/kube/src/api/core_methods.rs index 68da5193b..ef460645f 100644 --- a/kube/src/api/core_methods.rs +++ b/kube/src/api/core_methods.rs @@ -2,7 +2,6 @@ use either::Either; use futures::Stream; use serde::{de::DeserializeOwned, Serialize}; use std::fmt::Debug; -use tracing::instrument; use crate::{api::Api, Result}; use kube_core::{object::ObjectList, params::*, response::Status, WatchEvent}; @@ -25,9 +24,9 @@ where /// Ok(()) /// } /// ``` - #[instrument(skip(self), level = "trace")] pub async fn get(&self, name: &str) -> Result { - let req = self.request.get(name)?; + let mut req = self.request.get(name)?; + req.extensions_mut().insert("get"); self.client.request::(req).await } @@ -49,9 +48,9 @@ where /// Ok(()) /// } /// ``` - #[instrument(skip(self), level = "trace")] pub async fn list(&self, lp: &ListParams) -> Result> { - let req = self.request.list(&lp)?; + let mut req = self.request.list(&lp)?; + req.extensions_mut().insert("list"); self.client.request::>(req).await } @@ -71,13 +70,13 @@ where /// - Tradeoff between the two /// - Easy partially filling of native [`k8s_openapi`] types (most fields optional) /// - Partial safety against runtime errors (at least you must write valid JSON) - #[instrument(skip(self), level = "trace")] pub async fn create(&self, pp: &PostParams, data: &K) -> Result where K: Serialize, { let bytes = serde_json::to_vec(&data)?; - let req = self.request.create(&pp, bytes)?; + let mut req = self.request.create(&pp, bytes)?; + req.extensions_mut().insert("create"); self.client.request::(req).await } @@ -103,9 +102,9 @@ where /// Ok(()) /// } /// ``` - #[instrument(skip(self), level = "trace")] pub async fn delete(&self, name: &str, dp: &DeleteParams) -> Result> { - let req = self.request.delete(name, &dp)?; + let mut req = self.request.delete(name, &dp)?; + req.extensions_mut().insert("delete"); self.client.request_status::(req).await } @@ -136,13 +135,13 @@ where /// Ok(()) /// } /// ``` - #[instrument(skip(self), level = "trace")] pub async fn delete_collection( &self, dp: &DeleteParams, lp: &ListParams, ) -> Result, Status>> { - let req = self.request.delete_collection(&dp, &lp)?; + let mut req = self.request.delete_collection(&dp, &lp)?; + req.extensions_mut().insert("delete_collection"); self.client.request_status::>(req).await } @@ -175,14 +174,14 @@ where /// ``` /// [`Patch`]: super::Patch /// [`PatchParams`]: super::PatchParams - #[instrument(skip(self), level = "trace")] pub async fn patch( &self, name: &str, pp: &PatchParams, patch: &Patch

, ) -> Result { - let req = self.request.patch(name, &pp, patch)?; + let mut req = self.request.patch(name, &pp, patch)?; + req.extensions_mut().insert("patch"); self.client.request::(req).await } @@ -230,13 +229,13 @@ where /// ``` /// /// Consider mutating the result of `api.get` rather than recreating it. - #[instrument(skip(self), level = "trace")] pub async fn replace(&self, name: &str, pp: &PostParams, data: &K) -> Result where K: Serialize, { let bytes = serde_json::to_vec(&data)?; - let req = self.request.replace(name, &pp, bytes)?; + let mut req = self.request.replace(name, &pp, bytes)?; + req.extensions_mut().insert("replace"); self.client.request::(req).await } @@ -277,13 +276,13 @@ where /// ``` /// [`ListParams::timeout`]: super::ListParams::timeout /// [`watcher`]: https://docs.rs/kube_runtime/*/kube_runtime/watcher/fn.watcher.html - #[instrument(skip(self), level = "trace")] pub async fn watch( &self, lp: &ListParams, version: &str, ) -> Result>>> { - let req = self.request.watch(&lp, &version)?; + let mut req = self.request.watch(&lp, &version)?; + req.extensions_mut().insert("watch"); self.client.request_events::(req).await } } diff --git a/kube/src/api/subresource.rs b/kube/src/api/subresource.rs index c5f3ae6e9..5913d9134 100644 --- a/kube/src/api/subresource.rs +++ b/kube/src/api/subresource.rs @@ -2,7 +2,6 @@ use bytes::Bytes; use futures::Stream; use serde::de::DeserializeOwned; use std::fmt::Debug; -use tracing::instrument; use crate::{ api::{Api, Patch, PatchParams, PostParams}, @@ -24,28 +23,28 @@ where K: Clone + DeserializeOwned, { /// Fetch the scale subresource - #[instrument(skip(self), level = "trace")] pub async fn get_scale(&self, name: &str) -> Result { - let req = self.request.get_subresource("scale", name)?; + let mut req = self.request.get_subresource("scale", name)?; + req.extensions_mut().insert("get_scale"); self.client.request::(req).await } /// Update the scale subresource - #[instrument(skip(self), level = "trace")] pub async fn patch_scale( &self, name: &str, pp: &PatchParams, patch: &Patch

, ) -> Result { - let req = self.request.patch_subresource("scale", name, &pp, patch)?; + let mut req = self.request.patch_subresource("scale", name, &pp, patch)?; + req.extensions_mut().insert("patch_scale"); self.client.request::(req).await } /// Replace the scale subresource - #[instrument(skip(self), level = "trace")] pub async fn replace_scale(&self, name: &str, pp: &PostParams, data: Vec) -> Result { - let req = self.request.replace_subresource("scale", name, &pp, data)?; + let mut req = self.request.replace_subresource("scale", name, &pp, data)?; + req.extensions_mut().insert("replace_scale"); self.client.request::(req).await } } @@ -62,9 +61,9 @@ where /// Get the named resource with a status subresource /// /// This actually returns the whole K, with metadata, and spec. - #[instrument(skip(self), level = "trace")] pub async fn get_status(&self, name: &str) -> Result { - let req = self.request.get_subresource("status", name)?; + let mut req = self.request.get_subresource("status", name)?; + req.extensions_mut().insert("get_status"); self.client.request::(req).await } @@ -91,14 +90,14 @@ where /// Ok(()) /// } /// ``` - #[instrument(skip(self), level = "trace")] pub async fn patch_status( &self, name: &str, pp: &PatchParams, patch: &Patch

, ) -> Result { - let req = self.request.patch_subresource("status", name, &pp, patch)?; + let mut req = self.request.patch_subresource("status", name, &pp, patch)?; + req.extensions_mut().insert("patch_status"); self.client.request::(req).await } @@ -121,9 +120,9 @@ where /// Ok(()) /// } /// ``` - #[instrument(skip(self), level = "trace")] pub async fn replace_status(&self, name: &str, pp: &PostParams, data: Vec) -> Result { - let req = self.request.replace_subresource("status", name, &pp, data)?; + let mut req = self.request.replace_subresource("status", name, &pp, data)?; + req.extensions_mut().insert("replace_status"); self.client.request::(req).await } } @@ -155,16 +154,16 @@ where K: DeserializeOwned + Loggable, { /// Fetch logs as a string - #[instrument(skip(self), level = "trace")] pub async fn logs(&self, name: &str, lp: &LogParams) -> Result { - let req = self.request.logs(name, lp)?; + let mut req = self.request.logs(name, lp)?; + req.extensions_mut().insert("logs"); self.client.request_text(req).await } /// Fetch logs as a stream of bytes - #[instrument(skip(self), level = "trace")] pub async fn log_stream(&self, name: &str, lp: &LogParams) -> Result>> { - let req = self.request.logs(name, lp)?; + let mut req = self.request.logs(name, lp)?; + req.extensions_mut().insert("log_stream"); self.client.request_text_stream(req).await } } @@ -194,7 +193,8 @@ where { /// Create an eviction pub async fn evict(&self, name: &str, ep: &EvictParams) -> Result { - let req = self.request.evict(name, ep)?; + let mut req = self.request.evict(name, ep)?; + req.extensions_mut().insert("evict"); self.client.request::(req).await } } @@ -236,9 +236,9 @@ where K: Clone + DeserializeOwned + Attachable, { /// Attach to pod - #[instrument(skip(self), level = "trace")] pub async fn attach(&self, name: &str, ap: &AttachParams) -> Result { - let req = self.request.attach(name, ap)?; + let mut req = self.request.attach(name, ap)?; + req.extensions_mut().insert("attach"); let stream = self.client.connect(req).await?; Ok(AttachedProcess::new(stream, ap)) } @@ -282,7 +282,6 @@ where K: Clone + DeserializeOwned + Executable, { /// Execute a command in a pod - #[instrument(skip(self), level = "trace")] pub async fn exec( &self, name: &str, @@ -293,7 +292,8 @@ where I: IntoIterator, T: Into, { - let req = self.request.exec(name, command, ap)?; + let mut req = self.request.exec(name, command, ap)?; + req.extensions_mut().insert("exec"); let stream = self.client.connect(req).await?; Ok(AttachedProcess::new(stream, ap)) } diff --git a/kube/src/client/mod.rs b/kube/src/client/mod.rs index 7b5f9069a..b56c62a29 100644 --- a/kube/src/client/mod.rs +++ b/kube/src/client/mod.rs @@ -464,6 +464,7 @@ impl TryFrom for Client { http.method = %req.method(), http.url = %req.uri(), http.status_code = tracing::field::Empty, + otel.name = req.extensions().get::<&'static str>().unwrap_or(&"HTTP"), otel.kind = "client", otel.status_code = tracing::field::Empty, ) From 583cdbb02766b79d2b21f426aee44fe4c6a8ab46 Mon Sep 17 00:00:00 2001 From: kazk Date: Fri, 4 Jun 2021 12:08:26 -0700 Subject: [PATCH 25/36] Refactor the default service stack --- kube/src/client/mod.rs | 67 +++++++++++++++++++----------------------- 1 file changed, 31 insertions(+), 36 deletions(-) diff --git a/kube/src/client/mod.rs b/kube/src/client/mod.rs index b56c62a29..b1d800a7a 100644 --- a/kube/src/client/mod.rs +++ b/kube/src/client/mod.rs @@ -411,48 +411,43 @@ impl TryFrom for Client { let timeout = config.timeout; let default_ns = config.default_ns.clone(); - let common = ServiceBuilder::new() + let client: hyper::Client<_, Body> = { + let mut connector = HttpConnector::new(); + connector.enforce_http(false); + + // Note that if both `native_tls` and `rustls` is enabled, `native_tls` is used by default. + // To use `rustls`, disable `native_tls` or create custom client. + // If tls features are not enabled, http connector will be used. + #[cfg(feature = "native-tls")] + let connector = hyper_tls::HttpsConnector::from(( + connector, + tokio_native_tls::TlsConnector::from(config.native_tls_connector()?), + )); + #[cfg(all(not(feature = "native-tls"), feature = "rustls-tls"))] + let connector = hyper_rustls::HttpsConnector::from(( + connector, + std::sync::Arc::new(config.rustls_client_config()?), + )); + + let mut connector = TimeoutConnector::new(connector); + connector.set_connect_timeout(timeout); + connector.set_read_timeout(timeout); + + hyper::Client::builder().build(connector) + }; + + let stack = ServiceBuilder::new() .layer(SetBaseUriLayer::new(cluster_url)) .layer(SetHeadersLayer::new(default_headers)) .into_inner(); - #[cfg(feature = "gzip")] - let common = ServiceBuilder::new() - .layer(common) + let stack = ServiceBuilder::new() + .layer(stack) .layer(tower_http::decompression::DecompressionLayer::new()) .into_inner(); - let mut connector = HttpConnector::new(); - connector.enforce_http(false); - - // Note that if both `native_tls` and `rustls` is enabled, `native_tls` is used by default. - // To use `rustls`, disable `native_tls` or create custom client. - // If tls features are not enabled, http connector will be used. - #[cfg(feature = "native-tls")] - let connector = hyper_tls::HttpsConnector::from(( - connector, - tokio_native_tls::TlsConnector::from(config.native_tls_connector()?), - )); - - #[cfg(all(not(feature = "native-tls"), feature = "rustls-tls"))] - let connector = hyper_rustls::HttpsConnector::from(( - connector, - std::sync::Arc::new(config.rustls_client_config()?), - )); - - let mut connector = TimeoutConnector::new(connector); - if let Some(timeout) = timeout { - // reqwest's timeout is applied from when the request stars connecting until - // the response body has finished. - // Setting both connect and read timeout should be close enough. - connector.set_connect_timeout(Some(timeout)); - // Timeout for reading the response. - connector.set_read_timeout(Some(timeout)); - } - let client: hyper::Client<_, Body> = hyper::Client::builder().build(connector); - - let inner = ServiceBuilder::new() - .layer(common) + let service = ServiceBuilder::new() + .layer(stack) .option_layer(Authentication::try_from(&config.auth_info)?.into_layer()) .layer( // Attribute names follow [Semantic Conventions]. @@ -503,7 +498,7 @@ impl TryFrom for Client { }), ) .service(client); - Ok(Self::new_with_default_ns(inner, default_ns)) + Ok(Self::new_with_default_ns(service, default_ns)) } } From 54baa6123694d69f0100ed12391a36df32893d55 Mon Sep 17 00:00:00 2001 From: kazk Date: Fri, 4 Jun 2021 12:49:50 -0700 Subject: [PATCH 26/36] Rename Authentication to Auth --- kube/src/client/auth/mod.rs | 38 ++++++++++++++++++------------------- kube/src/client/mod.rs | 4 ++-- 2 files changed, 21 insertions(+), 21 deletions(-) diff --git a/kube/src/client/auth/mod.rs b/kube/src/client/auth/mod.rs index b6033d79a..53be07567 100644 --- a/kube/src/client/auth/mod.rs +++ b/kube/src/client/auth/mod.rs @@ -22,20 +22,20 @@ pub(crate) use refreshing_token::RefreshingTokenLayer; #[derive(Debug, Clone)] #[allow(clippy::large_enum_variant)] -pub(crate) enum Authentication { +pub(crate) enum Auth { None, Basic(String, String), Bearer(String), RefreshableToken(RefreshableToken), } -impl Authentication { +impl Auth { pub(crate) fn into_layer(self) -> Option> { match self { - Authentication::None => None, - Authentication::Basic(user, pass) => Some(Either::A(AddAuthorizationLayer::basic(&user, &pass))), - Authentication::Bearer(token) => Some(Either::A(AddAuthorizationLayer::bearer(&token))), - Authentication::RefreshableToken(r) => Some(Either::B(RefreshingTokenLayer::new(r))), + Self::None => None, + Self::Basic(user, pass) => Some(Either::A(AddAuthorizationLayer::basic(&user, &pass))), + Self::Bearer(token) => Some(Either::A(AddAuthorizationLayer::bearer(&token))), + Self::RefreshableToken(r) => Some(Either::B(RefreshingTokenLayer::new(r))), } } } @@ -61,12 +61,12 @@ impl RefreshableToken { // Add some wiggle room onto the current timestamp so we don't get any race // conditions where the token expires while we are refreshing if Utc::now() + Duration::seconds(60) >= locked_data.1 { - match Authentication::try_from(&locked_data.2)? { - Authentication::None | Authentication::Basic(_, _) | Authentication::Bearer(_) => { + match Auth::try_from(&locked_data.2)? { + Auth::None | Auth::Basic(_, _) | Auth::Bearer(_) => { return Err(ConfigError::UnrefreshableTokenResponse).map_err(Error::from); } - Authentication::RefreshableToken(RefreshableToken::Exec(d)) => { + Auth::RefreshableToken(RefreshableToken::Exec(d)) => { let (new_token, new_expire, new_info) = Arc::try_unwrap(d) .expect("Unable to unwrap Arc, this is likely a programming error") .into_inner(); @@ -77,7 +77,7 @@ impl RefreshableToken { // Unreachable because the token source does not change #[cfg(feature = "oauth")] - Authentication::RefreshableToken(RefreshableToken::GcpOauth(_)) => unreachable!(), + Auth::RefreshableToken(RefreshableToken::GcpOauth(_)) => unreachable!(), } } @@ -100,7 +100,7 @@ impl RefreshableToken { } } -impl TryFrom<&AuthInfo> for Authentication { +impl TryFrom<&AuthInfo> for Auth { type Error = Error; /// Loads the authentication header from the credentials available in the kubeconfig. This supports @@ -138,7 +138,7 @@ impl TryFrom<&AuthInfo> for Authentication { } if let (Some(u), Some(p)) = (&auth_info.username, &auth_info.password) { - return Ok(Authentication::Basic(u.to_owned(), p.to_owned())); + return Ok(Self::Basic(u.to_owned(), p.to_owned())); } let (raw_token, expiration) = match &auth_info.token { @@ -162,11 +162,11 @@ impl TryFrom<&AuthInfo> for Authentication { }; match (raw_token, expiration) { - (Some(token), None) => Ok(Authentication::Bearer(token)), - (Some(token), Some(expire)) => Ok(Authentication::RefreshableToken(RefreshableToken::Exec( - Arc::new(Mutex::new((token, expire, auth_info.clone()))), - ))), - _ => Ok(Authentication::None), + (Some(token), None) => Ok(Self::Bearer(token)), + (Some(token), Some(expire)) => Ok(Self::RefreshableToken(RefreshableToken::Exec(Arc::new( + Mutex::new((token, expire, auth_info.clone())), + )))), + _ => Ok(Self::None), } } } @@ -385,8 +385,8 @@ mod test { let config: Kubeconfig = serde_yaml::from_str(&test_file).map_err(ConfigError::ParseYaml)?; let auth_info = &config.auth_infos[0].auth_info; - match Authentication::try_from(auth_info).unwrap() { - Authentication::RefreshableToken(RefreshableToken::Exec(refreshable)) => { + match Auth::try_from(auth_info).unwrap() { + Auth::RefreshableToken(RefreshableToken::Exec(refreshable)) => { let (token, _expire, info) = Arc::try_unwrap(refreshable).unwrap().into_inner(); assert_eq!(token, "my_token".to_owned()); let config = info.auth_provider.unwrap().config; diff --git a/kube/src/client/mod.rs b/kube/src/client/mod.rs index b1d800a7a..a462d435e 100644 --- a/kube/src/client/mod.rs +++ b/kube/src/client/mod.rs @@ -34,7 +34,7 @@ use tower_http::{ use crate::{api::WatchEvent, error::ErrorResponse, Config, Error, Result}; mod auth; -use auth::Authentication; +use auth::Auth; mod base_uri; pub use base_uri::{SetBaseUri, SetBaseUriLayer}; mod body; @@ -448,7 +448,7 @@ impl TryFrom for Client { let service = ServiceBuilder::new() .layer(stack) - .option_layer(Authentication::try_from(&config.auth_info)?.into_layer()) + .option_layer(Auth::try_from(&config.auth_info)?.into_layer()) .layer( // Attribute names follow [Semantic Conventions]. // [Semantic Conventions]: https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/trace/semantic_conventions/http.md From 0514408ed7093b03c43e3fbc02705584f5bebacf Mon Sep 17 00:00:00 2001 From: kazk Date: Fri, 4 Jun 2021 13:32:27 -0700 Subject: [PATCH 27/36] Add `client::ConfigExt` to extend `Config` for `Client` - Move TLS methods to `ConfigExt` - Prepare to move `Auth` method to `ConfigExt` - `.option_layer(config.auth_layer()?)` --- examples/custom_client.rs | 2 +- examples/custom_client_tls.rs | 2 +- examples/custom_client_trace.rs | 2 +- kube/src/client/auth/mod.rs | 12 ---- kube/src/client/config_ext.rs | 92 ++++++++++++++++++++++++++++++ kube/src/client/mod.rs | 5 +- kube/src/{config => client}/tls.rs | 57 +----------------- kube/src/config/mod.rs | 3 +- 8 files changed, 102 insertions(+), 73 deletions(-) create mode 100644 kube/src/client/config_ext.rs rename kube/src/{config => client}/tls.rs (74%) diff --git a/examples/custom_client.rs b/examples/custom_client.rs index 098add9a5..8f3c65cc5 100644 --- a/examples/custom_client.rs +++ b/examples/custom_client.rs @@ -4,7 +4,7 @@ use tower::ServiceBuilder; use kube::{ api::{Api, ListParams}, - client::SetBaseUriLayer, + client::{ConfigExt, SetBaseUriLayer}, Client, Config, }; diff --git a/examples/custom_client_tls.rs b/examples/custom_client_tls.rs index a4c7c8166..d3d793c19 100644 --- a/examples/custom_client_tls.rs +++ b/examples/custom_client_tls.rs @@ -6,7 +6,7 @@ use tower::ServiceBuilder; use kube::{ api::{Api, ListParams}, - client::SetBaseUriLayer, + client::{ConfigExt, SetBaseUriLayer}, Client, Config, }; diff --git a/examples/custom_client_trace.rs b/examples/custom_client_trace.rs index 7c1f480e6..5abb0eeb3 100644 --- a/examples/custom_client_trace.rs +++ b/examples/custom_client_trace.rs @@ -10,7 +10,7 @@ use tracing::Span; use kube::{ api::{Api, ListParams}, - client::SetBaseUriLayer, + client::{ConfigExt, SetBaseUriLayer}, Client, Config, }; diff --git a/kube/src/client/auth/mod.rs b/kube/src/client/auth/mod.rs index 53be07567..a7a7512b9 100644 --- a/kube/src/client/auth/mod.rs +++ b/kube/src/client/auth/mod.rs @@ -5,7 +5,6 @@ use http::HeaderValue; use jsonpath_lib::select as jsonpath_select; use serde::{Deserialize, Serialize}; use tokio::sync::Mutex; -use tower::util::Either; use crate::{ config::{read_file_to_string, AuthInfo, AuthProviderConfig, ExecConfig}, @@ -29,17 +28,6 @@ pub(crate) enum Auth { RefreshableToken(RefreshableToken), } -impl Auth { - pub(crate) fn into_layer(self) -> Option> { - match self { - Self::None => None, - Self::Basic(user, pass) => Some(Either::A(AddAuthorizationLayer::basic(&user, &pass))), - Self::Bearer(token) => Some(Either::A(AddAuthorizationLayer::bearer(&token))), - Self::RefreshableToken(r) => Some(Either::B(RefreshingTokenLayer::new(r))), - } - } -} - // See https://github.com/kubernetes/kubernetes/tree/master/staging/src/k8s.io/client-go/plugin/pkg/client/auth // for the list of auth-plugins supported by client-go. // We currently support the following: diff --git a/kube/src/client/config_ext.rs b/kube/src/client/config_ext.rs new file mode 100644 index 000000000..7d22a0004 --- /dev/null +++ b/kube/src/client/config_ext.rs @@ -0,0 +1,92 @@ +use std::convert::TryFrom; + +use tower::util::Either; + +#[cfg(any(feature = "native-tls", feature = "rustls-tls"))] use super::tls; +use super::{ + auth::{AddAuthorizationLayer, RefreshingTokenLayer}, + Auth, +}; +use crate::{Config, Result}; + +/// Extensions to `Config` for `Client`. +/// +/// This trait is sealed and cannot be implemented. +pub trait ConfigExt: private::Sealed { + /// Create `native_tls::TlsConnector` + #[cfg_attr(docsrs, doc(cfg(feature = "native-tls")))] + #[cfg(feature = "native-tls")] + fn native_tls_connector(&self) -> Result; + + /// Create `hyper_tls::HttpsConnector` + #[cfg_attr(docsrs, doc(cfg(feature = "native-tls")))] + #[cfg(feature = "native-tls")] + fn native_tls_https_connector(&self) -> Result>; + + /// Create `rustls::ClientConfig` + #[cfg_attr(docsrs, doc(cfg(feature = "rustls-tls")))] + #[cfg(feature = "rustls-tls")] + fn rustls_client_config(&self) -> Result; + + /// Create `hyper_rustls::HttpsConnector` + #[cfg_attr(docsrs, doc(cfg(feature = "rustls-tls")))] + #[cfg(feature = "rustls-tls")] + fn rustls_https_connector(&self) -> Result>; + + // TODO Try reducing exported types to minimize API surface before making this public. + #[doc(hidden)] + /// Optional layer to set up `Authorization` header depending on the config. + /// + /// Users are not allowed to call this for now. + fn auth_layer(&self) -> Result>>; +} + +mod private { + pub trait Sealed {} + impl Sealed for super::Config {} +} + +impl ConfigExt for Config { + #[cfg(feature = "native-tls")] + fn native_tls_connector(&self) -> Result { + tls::native_tls::native_tls_connector( + self.identity_pem.as_ref(), + self.root_cert.as_ref(), + self.accept_invalid_certs, + ) + } + + #[cfg(feature = "native-tls")] + fn native_tls_https_connector(&self) -> Result> { + let tls = tokio_native_tls::TlsConnector::from(self.native_tls_connector()?); + let mut http = hyper::client::HttpConnector::new(); + http.enforce_http(false); + Ok(hyper_tls::HttpsConnector::from((http, tls))) + } + + #[cfg(feature = "rustls-tls")] + fn rustls_client_config(&self) -> Result { + tls::rustls_tls::rustls_client_config( + self.identity_pem.as_ref(), + self.root_cert.as_ref(), + self.accept_invalid_certs, + ) + } + + #[cfg(feature = "rustls-tls")] + fn rustls_https_connector(&self) -> Result> { + let rustls_config = std::sync::Arc::new(self.rustls_client_config()?); + let mut http = hyper::client::HttpConnector::new(); + http.enforce_http(false); + Ok(hyper_rustls::HttpsConnector::from((http, rustls_config))) + } + + fn auth_layer(&self) -> Result>> { + Ok(match Auth::try_from(&self.auth_info)? { + Auth::None => None, + Auth::Basic(user, pass) => Some(Either::A(AddAuthorizationLayer::basic(&user, &pass))), + Auth::Bearer(token) => Some(Either::A(AddAuthorizationLayer::bearer(&token))), + Auth::RefreshableToken(r) => Some(Either::B(RefreshingTokenLayer::new(r))), + }) + } +} diff --git a/kube/src/client/mod.rs b/kube/src/client/mod.rs index a462d435e..66697d05e 100644 --- a/kube/src/client/mod.rs +++ b/kube/src/client/mod.rs @@ -40,8 +40,11 @@ pub use base_uri::{SetBaseUri, SetBaseUriLayer}; mod body; // Add `into_stream()` to `http::Body` use body::BodyStreamExt; +mod config_ext; +pub use config_ext::ConfigExt; mod headers; use headers::SetHeadersLayer; +#[cfg(any(feature = "native-tls", feature = "rustls-tls"))] mod tls; // Binary subprotocol v4. See `Client::connect`. #[cfg(feature = "ws")] @@ -448,7 +451,7 @@ impl TryFrom for Client { let service = ServiceBuilder::new() .layer(stack) - .option_layer(Auth::try_from(&config.auth_info)?.into_layer()) + .option_layer(config.auth_layer()?) .layer( // Attribute names follow [Semantic Conventions]. // [Semantic Conventions]: https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/trace/semantic_conventions/http.md diff --git a/kube/src/config/tls.rs b/kube/src/client/tls.rs similarity index 74% rename from kube/src/config/tls.rs rename to kube/src/client/tls.rs index a5c4bf074..3d749b8d8 100644 --- a/kube/src/config/tls.rs +++ b/kube/src/client/tls.rs @@ -1,58 +1,5 @@ -use crate::Result; - -use super::Config; - -impl Config { - /// Create `native_tls::TlsConnector` - #[cfg_attr(docsrs, doc(cfg(feature = "native-tls")))] - #[cfg(feature = "native-tls")] - pub fn native_tls_connector(&self) -> Result { - self::native_tls::native_tls_connector( - self.identity_pem.as_ref(), - self.root_cert.as_ref(), - self.accept_invalid_certs, - ) - } - - /// Create `hyper_tls::HttpsConnector` - #[cfg_attr(docsrs, doc(cfg(all(feature = "client", feature = "native-tls"))))] - #[cfg(all(feature = "client", feature = "native-tls"))] - pub fn native_tls_https_connector( - &self, - ) -> Result> { - let tls = tokio_native_tls::TlsConnector::from(self.native_tls_connector()?); - let mut http = hyper::client::HttpConnector::new(); - http.enforce_http(false); - Ok(hyper_tls::HttpsConnector::from((http, tls))) - } - - /// Create `rustls::ClientConfig` - #[cfg_attr(docsrs, doc(cfg(feature = "rustls-tls")))] - #[cfg(feature = "rustls-tls")] - pub fn rustls_client_config(&self) -> Result { - self::rustls_tls::rustls_client_config( - self.identity_pem.as_ref(), - self.root_cert.as_ref(), - self.accept_invalid_certs, - ) - } - - /// Create `hyper_rustls::HttpsConnector` - #[cfg_attr(docsrs, doc(cfg(all(feature = "client", feature = "rustls-tls"))))] - #[cfg(all(feature = "client", feature = "rustls-tls"))] - pub fn rustls_https_connector( - &self, - ) -> Result> { - let rustls_config = std::sync::Arc::new(self.rustls_client_config()?); - let mut http = hyper::client::HttpConnector::new(); - http.enforce_http(false); - Ok(hyper_rustls::HttpsConnector::from((http, rustls_config))) - } -} - - #[cfg(feature = "native-tls")] -mod native_tls { +pub mod native_tls { use tokio_native_tls::native_tls::{Certificate, Identity, TlsConnector}; use crate::{Error, Result}; @@ -102,7 +49,7 @@ mod native_tls { } #[cfg(feature = "rustls-tls")] -mod rustls_tls { +pub mod rustls_tls { use std::sync::Arc; use rustls::{self, Certificate, ClientConfig, ServerCertVerified, ServerCertVerifier}; diff --git a/kube/src/config/mod.rs b/kube/src/config/mod.rs index 0c1b6524a..d59b5f25a 100644 --- a/kube/src/config/mod.rs +++ b/kube/src/config/mod.rs @@ -7,7 +7,6 @@ mod file_config; mod file_loader; mod incluster_config; -#[cfg(any(feature = "native-tls", feature = "rustls-tls"))] mod tls; mod utils; use crate::{error::ConfigError, Result}; @@ -38,7 +37,7 @@ pub struct Config { pub accept_invalid_certs: bool, // TODO should keep client key and certificate separate. It's split later anyway. /// Client certificate and private key in PEM. - identity_pem: Option>, + pub(crate) identity_pem: Option>, /// Stores information to tell the cluster who you are. pub(crate) auth_info: AuthInfo, } From b6f939d7496b2fdb73431e3b8a613ad47f29e777 Mon Sep 17 00:00:00 2001 From: kazk Date: Fri, 4 Jun 2021 14:34:31 -0700 Subject: [PATCH 28/36] Add `ConfigExt::base_uri_layer` --- examples/custom_client.rs | 4 ++-- examples/custom_client_tls.rs | 6 +++--- examples/custom_client_trace.rs | 4 ++-- kube/src/client/config_ext.rs | 9 ++++++++- kube/src/client/mod.rs | 3 +-- 5 files changed, 16 insertions(+), 10 deletions(-) diff --git a/examples/custom_client.rs b/examples/custom_client.rs index 8f3c65cc5..b035fa9d1 100644 --- a/examples/custom_client.rs +++ b/examples/custom_client.rs @@ -4,7 +4,7 @@ use tower::ServiceBuilder; use kube::{ api::{Api, ListParams}, - client::{ConfigExt, SetBaseUriLayer}, + client::ConfigExt, Client, Config, }; @@ -17,7 +17,7 @@ async fn main() -> anyhow::Result<()> { let https = config.native_tls_https_connector()?; let client = Client::new( ServiceBuilder::new() - .layer(SetBaseUriLayer::new(config.cluster_url)) + .layer(config.base_uri_layer()) .service(hyper::Client::builder().build(https)), ); diff --git a/examples/custom_client_tls.rs b/examples/custom_client_tls.rs index d3d793c19..c1ccfa60d 100644 --- a/examples/custom_client_tls.rs +++ b/examples/custom_client_tls.rs @@ -6,7 +6,7 @@ use tower::ServiceBuilder; use kube::{ api::{Api, ListParams}, - client::{ConfigExt, SetBaseUriLayer}, + client::ConfigExt, Client, Config, }; @@ -23,14 +23,14 @@ async fn main() -> anyhow::Result<()> { let https = config.rustls_https_connector()?; Client::new( ServiceBuilder::new() - .layer(SetBaseUriLayer::new(config.cluster_url)) + .layer(config.base_uri_layer()) .service(hyper::Client::builder().build(https)), ) } else { let https = config.native_tls_https_connector()?; Client::new( ServiceBuilder::new() - .layer(SetBaseUriLayer::new(config.cluster_url)) + .layer(config.base_uri_layer()) .service(hyper::Client::builder().build(https)), ) }; diff --git a/examples/custom_client_trace.rs b/examples/custom_client_trace.rs index 5abb0eeb3..458fb0e93 100644 --- a/examples/custom_client_trace.rs +++ b/examples/custom_client_trace.rs @@ -10,7 +10,7 @@ use tracing::Span; use kube::{ api::{Api, ListParams}, - client::{ConfigExt, SetBaseUriLayer}, + client::ConfigExt, Client, Config, }; @@ -23,7 +23,7 @@ async fn main() -> anyhow::Result<()> { let https = config.native_tls_https_connector()?; let client = Client::new( ServiceBuilder::new() - .layer(SetBaseUriLayer::new(config.cluster_url)) + .layer(config.base_uri_layer()) // Add `DecompressionLayer` to make request headers interesting. .layer(DecompressionLayer::new()) .layer( diff --git a/kube/src/client/config_ext.rs b/kube/src/client/config_ext.rs index 7d22a0004..48c2625aa 100644 --- a/kube/src/client/config_ext.rs +++ b/kube/src/client/config_ext.rs @@ -5,7 +5,7 @@ use tower::util::Either; #[cfg(any(feature = "native-tls", feature = "rustls-tls"))] use super::tls; use super::{ auth::{AddAuthorizationLayer, RefreshingTokenLayer}, - Auth, + Auth, SetBaseUriLayer, }; use crate::{Config, Result}; @@ -13,6 +13,9 @@ use crate::{Config, Result}; /// /// This trait is sealed and cannot be implemented. pub trait ConfigExt: private::Sealed { + /// Layer to set the base URI of requests to the configured server. + fn base_uri_layer(&self) -> SetBaseUriLayer; + /// Create `native_tls::TlsConnector` #[cfg_attr(docsrs, doc(cfg(feature = "native-tls")))] #[cfg(feature = "native-tls")] @@ -47,6 +50,10 @@ mod private { } impl ConfigExt for Config { + fn base_uri_layer(&self) -> SetBaseUriLayer { + SetBaseUriLayer::new(self.cluster_url.clone()) + } + #[cfg(feature = "native-tls")] fn native_tls_connector(&self) -> Result { tls::native_tls::native_tls_connector( diff --git a/kube/src/client/mod.rs b/kube/src/client/mod.rs index 66697d05e..ee7bfa491 100644 --- a/kube/src/client/mod.rs +++ b/kube/src/client/mod.rs @@ -409,7 +409,6 @@ impl TryFrom for Client { use http::header::HeaderMap; use tracing::Span; - let cluster_url = config.cluster_url.clone(); let default_headers = config.headers.clone(); let timeout = config.timeout; let default_ns = config.default_ns.clone(); @@ -440,7 +439,7 @@ impl TryFrom for Client { }; let stack = ServiceBuilder::new() - .layer(SetBaseUriLayer::new(cluster_url)) + .layer(config.base_uri_layer()) .layer(SetHeadersLayer::new(default_headers)) .into_inner(); #[cfg(feature = "gzip")] From 225875090973e024a5b27472f22e83fea09c3579 Mon Sep 17 00:00:00 2001 From: kazk Date: Fri, 4 Jun 2021 15:27:28 -0700 Subject: [PATCH 29/36] Rename to `RefreshTokenLayer` --- kube/src/client/auth/mod.rs | 4 +-- .../{refreshing_token.rs => refresh_token.rs} | 30 +++++++++---------- kube/src/client/config_ext.rs | 8 ++--- 3 files changed, 21 insertions(+), 21 deletions(-) rename kube/src/client/auth/{refreshing_token.rs => refresh_token.rs} (90%) diff --git a/kube/src/client/auth/mod.rs b/kube/src/client/auth/mod.rs index a7a7512b9..e012df720 100644 --- a/kube/src/client/auth/mod.rs +++ b/kube/src/client/auth/mod.rs @@ -15,9 +15,9 @@ use crate::{ #[cfg(feature = "oauth")] mod oauth; mod add_authorization; -mod refreshing_token; +mod refresh_token; pub(crate) use add_authorization::AddAuthorizationLayer; -pub(crate) use refreshing_token::RefreshingTokenLayer; +pub(crate) use refresh_token::RefreshTokenLayer; #[derive(Debug, Clone)] #[allow(clippy::large_enum_variant)] diff --git a/kube/src/client/auth/refreshing_token.rs b/kube/src/client/auth/refresh_token.rs similarity index 90% rename from kube/src/client/auth/refreshing_token.rs rename to kube/src/client/auth/refresh_token.rs index ff5e204b7..f4cfd38fa 100644 --- a/kube/src/client/auth/refreshing_token.rs +++ b/kube/src/client/auth/refresh_token.rs @@ -11,35 +11,35 @@ use tower::{layer::Layer, BoxError, Service}; use super::RefreshableToken; use crate::Result; -// TODO Come up with a better name -/// `Layer` to decorate the request with `Authorization` header with token refreshing automatically. -pub struct RefreshingTokenLayer { +/// `Layer` to decorate the request with `Authorization` header with refreshable token. +/// Token is refreshed automatically when necessary. +pub struct RefreshTokenLayer { refreshable: RefreshableToken, } -impl RefreshingTokenLayer { +impl RefreshTokenLayer { pub(crate) fn new(refreshable: RefreshableToken) -> Self { Self { refreshable } } } -impl Layer for RefreshingTokenLayer { - type Service = RefreshingToken; +impl Layer for RefreshTokenLayer { + type Service = RefreshToken; fn layer(&self, service: S) -> Self::Service { - RefreshingToken { + RefreshToken { refreshable: self.refreshable.clone(), service, } } } -pub struct RefreshingToken { +pub struct RefreshToken { refreshable: RefreshableToken, service: S, } -impl Service> for RefreshingToken +impl Service> for RefreshToken where S: Service, Response = Response> + Clone, S::Error: Into, @@ -47,7 +47,7 @@ where ResB: http_body::Body, { type Error = BoxError; - type Future = AuthFuture; + type Future = RefreshTokenFuture; type Response = S::Response; fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll> { @@ -71,7 +71,7 @@ where }) }; - AuthFuture { + RefreshTokenFuture { state: State::Request(Box::pin(request)), service, } @@ -90,7 +90,7 @@ enum State { type RequestFuture = Pin, BoxError>> + Send>>; #[pin_project] -pub struct AuthFuture +pub struct RefreshTokenFuture where S: Service>, B: http_body::Body, @@ -100,7 +100,7 @@ where service: S, } -impl Future for AuthFuture +impl Future for RefreshTokenFuture where S: Service>, S::Error: Into, @@ -148,7 +148,7 @@ mod tests { const TOKEN: &str = "test"; let auth = test_token(TOKEN.into()); let (mut service, handle): (_, Handle, Response>) = - mock::spawn_layer(RefreshingTokenLayer::new(auth)); + mock::spawn_layer(RefreshTokenLayer::new(auth)); let spawned = tokio::spawn(async move { // Receive the requests and respond @@ -174,7 +174,7 @@ mod tests { const TOKEN: &str = "\n"; let auth = test_token(TOKEN.into()); let (mut service, _handle) = - mock::spawn_layer::, Response, _>(RefreshingTokenLayer::new(auth)); + mock::spawn_layer::, Response, _>(RefreshTokenLayer::new(auth)); let err = service .call(Request::builder().uri("/").body(Body::empty()).unwrap()) .await diff --git a/kube/src/client/config_ext.rs b/kube/src/client/config_ext.rs index 48c2625aa..48cc203cc 100644 --- a/kube/src/client/config_ext.rs +++ b/kube/src/client/config_ext.rs @@ -4,7 +4,7 @@ use tower::util::Either; #[cfg(any(feature = "native-tls", feature = "rustls-tls"))] use super::tls; use super::{ - auth::{AddAuthorizationLayer, RefreshingTokenLayer}, + auth::{AddAuthorizationLayer, RefreshTokenLayer}, Auth, SetBaseUriLayer, }; use crate::{Config, Result}; @@ -41,7 +41,7 @@ pub trait ConfigExt: private::Sealed { /// Optional layer to set up `Authorization` header depending on the config. /// /// Users are not allowed to call this for now. - fn auth_layer(&self) -> Result>>; + fn auth_layer(&self) -> Result>>; } mod private { @@ -88,12 +88,12 @@ impl ConfigExt for Config { Ok(hyper_rustls::HttpsConnector::from((http, rustls_config))) } - fn auth_layer(&self) -> Result>> { + fn auth_layer(&self) -> Result>> { Ok(match Auth::try_from(&self.auth_info)? { Auth::None => None, Auth::Basic(user, pass) => Some(Either::A(AddAuthorizationLayer::basic(&user, &pass))), Auth::Bearer(token) => Some(Either::A(AddAuthorizationLayer::bearer(&token))), - Auth::RefreshableToken(r) => Some(Either::B(RefreshingTokenLayer::new(r))), + Auth::RefreshableToken(r) => Some(Either::B(RefreshTokenLayer::new(r))), }) } } From 0f0994e25d99f67aa7a38fd80993c6fbd9c0407e Mon Sep 17 00:00:00 2001 From: kazk Date: Fri, 4 Jun 2021 16:15:26 -0700 Subject: [PATCH 30/36] Discourage using middleware directly --- kube/src/client/auth/mod.rs | 5 ----- kube/src/client/config_ext.rs | 4 ++-- .../client/{auth => middleware}/add_authorization.rs | 0 kube/src/client/{ => middleware}/base_uri.rs | 0 kube/src/client/{ => middleware}/headers.rs | 1 + kube/src/client/middleware/mod.rs | 10 ++++++++++ kube/src/client/{auth => middleware}/refresh_token.rs | 3 +-- kube/src/client/mod.rs | 7 ++----- 8 files changed, 16 insertions(+), 14 deletions(-) rename kube/src/client/{auth => middleware}/add_authorization.rs (100%) rename kube/src/client/{ => middleware}/base_uri.rs (100%) rename kube/src/client/{ => middleware}/headers.rs (96%) create mode 100644 kube/src/client/middleware/mod.rs rename kube/src/client/{auth => middleware}/refresh_token.rs (99%) diff --git a/kube/src/client/auth/mod.rs b/kube/src/client/auth/mod.rs index e012df720..53c372b38 100644 --- a/kube/src/client/auth/mod.rs +++ b/kube/src/client/auth/mod.rs @@ -14,11 +14,6 @@ use crate::{ #[cfg(feature = "oauth")] mod oauth; -mod add_authorization; -mod refresh_token; -pub(crate) use add_authorization::AddAuthorizationLayer; -pub(crate) use refresh_token::RefreshTokenLayer; - #[derive(Debug, Clone)] #[allow(clippy::large_enum_variant)] pub(crate) enum Auth { diff --git a/kube/src/client/config_ext.rs b/kube/src/client/config_ext.rs index 48cc203cc..eac05bf7f 100644 --- a/kube/src/client/config_ext.rs +++ b/kube/src/client/config_ext.rs @@ -4,8 +4,8 @@ use tower::util::Either; #[cfg(any(feature = "native-tls", feature = "rustls-tls"))] use super::tls; use super::{ - auth::{AddAuthorizationLayer, RefreshTokenLayer}, - Auth, SetBaseUriLayer, + auth::Auth, + middleware::{AddAuthorizationLayer, RefreshTokenLayer, SetBaseUriLayer}, }; use crate::{Config, Result}; diff --git a/kube/src/client/auth/add_authorization.rs b/kube/src/client/middleware/add_authorization.rs similarity index 100% rename from kube/src/client/auth/add_authorization.rs rename to kube/src/client/middleware/add_authorization.rs diff --git a/kube/src/client/base_uri.rs b/kube/src/client/middleware/base_uri.rs similarity index 100% rename from kube/src/client/base_uri.rs rename to kube/src/client/middleware/base_uri.rs diff --git a/kube/src/client/headers.rs b/kube/src/client/middleware/headers.rs similarity index 96% rename from kube/src/client/headers.rs rename to kube/src/client/middleware/headers.rs index dd624002d..4b29e1e61 100644 --- a/kube/src/client/headers.rs +++ b/kube/src/client/middleware/headers.rs @@ -1,6 +1,7 @@ use http::{header::HeaderMap, Request}; use tower::{Layer, Service}; +// TODO Remove this and `headers` field from `Config`. /// Layer that applies [`SetHeaders`] which sets the provided headers to each request. #[derive(Debug, Clone)] pub struct SetHeadersLayer { diff --git a/kube/src/client/middleware/mod.rs b/kube/src/client/middleware/mod.rs new file mode 100644 index 000000000..8320e5912 --- /dev/null +++ b/kube/src/client/middleware/mod.rs @@ -0,0 +1,10 @@ +//! Middleware types returned from `ConfigExt` methods. +mod add_authorization; +mod base_uri; +mod headers; +mod refresh_token; + +pub(crate) use add_authorization::AddAuthorizationLayer; +pub use base_uri::{SetBaseUri, SetBaseUriLayer}; +pub use headers::{SetHeaders, SetHeadersLayer}; +pub(crate) use refresh_token::RefreshTokenLayer; diff --git a/kube/src/client/auth/refresh_token.rs b/kube/src/client/middleware/refresh_token.rs similarity index 99% rename from kube/src/client/auth/refresh_token.rs rename to kube/src/client/middleware/refresh_token.rs index f4cfd38fa..e73bba931 100644 --- a/kube/src/client/auth/refresh_token.rs +++ b/kube/src/client/middleware/refresh_token.rs @@ -8,8 +8,7 @@ use http::{header::AUTHORIZATION, Request, Response}; use pin_project::pin_project; use tower::{layer::Layer, BoxError, Service}; -use super::RefreshableToken; -use crate::Result; +use crate::{client::auth::RefreshableToken, Result}; /// `Layer` to decorate the request with `Authorization` header with refreshable token. /// Token is refreshed automatically when necessary. diff --git a/kube/src/client/mod.rs b/kube/src/client/mod.rs index ee7bfa491..84fc9934b 100644 --- a/kube/src/client/mod.rs +++ b/kube/src/client/mod.rs @@ -34,16 +34,13 @@ use tower_http::{ use crate::{api::WatchEvent, error::ErrorResponse, Config, Error, Result}; mod auth; -use auth::Auth; -mod base_uri; -pub use base_uri::{SetBaseUri, SetBaseUriLayer}; mod body; // Add `into_stream()` to `http::Body` use body::BodyStreamExt; mod config_ext; pub use config_ext::ConfigExt; -mod headers; -use headers::SetHeadersLayer; +pub mod middleware; +use middleware::SetHeadersLayer; #[cfg(any(feature = "native-tls", feature = "rustls-tls"))] mod tls; // Binary subprotocol v4. See `Client::connect`. From 1b089271b77d191a52938eefa7300b86b899e221 Mon Sep 17 00:00:00 2001 From: kazk Date: Fri, 4 Jun 2021 16:18:40 -0700 Subject: [PATCH 31/36] Remove `headers` from `Config` --- kube/src/client/middleware/headers.rs | 55 --------------------------- kube/src/client/middleware/mod.rs | 2 - kube/src/client/mod.rs | 7 +--- kube/src/config/mod.rs | 7 ---- 4 files changed, 1 insertion(+), 70 deletions(-) delete mode 100644 kube/src/client/middleware/headers.rs diff --git a/kube/src/client/middleware/headers.rs b/kube/src/client/middleware/headers.rs deleted file mode 100644 index 4b29e1e61..000000000 --- a/kube/src/client/middleware/headers.rs +++ /dev/null @@ -1,55 +0,0 @@ -use http::{header::HeaderMap, Request}; -use tower::{Layer, Service}; - -// TODO Remove this and `headers` field from `Config`. -/// Layer that applies [`SetHeaders`] which sets the provided headers to each request. -#[derive(Debug, Clone)] -pub struct SetHeadersLayer { - headers: HeaderMap, -} - -impl SetHeadersLayer { - /// Create a new [`SetHeadersLayer`]. - pub fn new(headers: HeaderMap) -> Self { - Self { headers } - } -} - -impl Layer for SetHeadersLayer { - type Service = SetHeaders; - - fn layer(&self, inner: S) -> Self::Service { - SetHeaders { - headers: self.headers.clone(), - inner, - } - } -} - -/// Middleware that set headers. -#[derive(Debug, Clone)] -pub struct SetHeaders { - headers: HeaderMap, - inner: S, -} - -impl Service> for SetHeaders -where - S: Service>, -{ - type Error = S::Error; - type Future = S::Future; - type Response = S::Response; - - fn poll_ready(&mut self, cx: &mut std::task::Context<'_>) -> std::task::Poll> { - self.inner.poll_ready(cx) - } - - fn call(&mut self, req: Request) -> Self::Future { - let (mut parts, body) = req.into_parts(); - let mut headers = self.headers.clone(); - headers.extend(parts.headers.into_iter()); - parts.headers = headers; - self.inner.call(Request::from_parts(parts, body)) - } -} diff --git a/kube/src/client/middleware/mod.rs b/kube/src/client/middleware/mod.rs index 8320e5912..1b88b16f5 100644 --- a/kube/src/client/middleware/mod.rs +++ b/kube/src/client/middleware/mod.rs @@ -1,10 +1,8 @@ //! Middleware types returned from `ConfigExt` methods. mod add_authorization; mod base_uri; -mod headers; mod refresh_token; pub(crate) use add_authorization::AddAuthorizationLayer; pub use base_uri::{SetBaseUri, SetBaseUriLayer}; -pub use headers::{SetHeaders, SetHeadersLayer}; pub(crate) use refresh_token::RefreshTokenLayer; diff --git a/kube/src/client/mod.rs b/kube/src/client/mod.rs index 84fc9934b..af03b56b9 100644 --- a/kube/src/client/mod.rs +++ b/kube/src/client/mod.rs @@ -40,7 +40,6 @@ use body::BodyStreamExt; mod config_ext; pub use config_ext::ConfigExt; pub mod middleware; -use middleware::SetHeadersLayer; #[cfg(any(feature = "native-tls", feature = "rustls-tls"))] mod tls; // Binary subprotocol v4. See `Client::connect`. @@ -406,7 +405,6 @@ impl TryFrom for Client { use http::header::HeaderMap; use tracing::Span; - let default_headers = config.headers.clone(); let timeout = config.timeout; let default_ns = config.default_ns.clone(); @@ -435,10 +433,7 @@ impl TryFrom for Client { hyper::Client::builder().build(connector) }; - let stack = ServiceBuilder::new() - .layer(config.base_uri_layer()) - .layer(SetHeadersLayer::new(default_headers)) - .into_inner(); + let stack = ServiceBuilder::new().layer(config.base_uri_layer()).into_inner(); #[cfg(feature = "gzip")] let stack = ServiceBuilder::new() .layer(stack) diff --git a/kube/src/config/mod.rs b/kube/src/config/mod.rs index d59b5f25a..0520e9b20 100644 --- a/kube/src/config/mod.rs +++ b/kube/src/config/mod.rs @@ -14,8 +14,6 @@ use file_loader::ConfigLoader; pub use file_loader::KubeConfigOptions; #[cfg(feature = "client")] pub(crate) use utils::read_file_to_string; -use http::header::HeaderMap; - use std::time::Duration; /// Configuration object detailing things like cluster URL, default namespace, root certificates, and timeouts. @@ -27,8 +25,6 @@ pub struct Config { pub default_ns: String, /// The configured root certificate pub root_cert: Option>>, - /// Default headers to be used to communicate with the Kubernetes API - pub headers: HeaderMap, /// Timeout for calls to the Kubernetes API. /// /// A value of `None` means no timeout @@ -53,7 +49,6 @@ impl Config { cluster_url, default_ns: String::from("default"), root_cert: None, - headers: HeaderMap::new(), timeout: Some(DEFAULT_TIMEOUT), accept_invalid_certs: false, identity_pem: None, @@ -111,7 +106,6 @@ impl Config { cluster_url, default_ns, root_cert: Some(root_cert), - headers: HeaderMap::new(), timeout: Some(DEFAULT_TIMEOUT), accept_invalid_certs: false, identity_pem: None, @@ -177,7 +171,6 @@ impl Config { cluster_url, default_ns, root_cert, - headers: HeaderMap::new(), timeout: Some(DEFAULT_TIMEOUT), accept_invalid_certs, identity_pem, From f009ff68b012f1d047fcb109b0c565e121bac4cc Mon Sep 17 00:00:00 2001 From: kazk Date: Fri, 4 Jun 2021 17:34:16 -0700 Subject: [PATCH 32/36] Support loading proxy URL --- kube/src/config/file_config.rs | 3 +++ kube/src/config/file_loader.rs | 13 +++++++++++++ kube/src/config/mod.rs | 6 ++++++ 3 files changed, 22 insertions(+) diff --git a/kube/src/config/file_config.rs b/kube/src/config/file_config.rs index 8d7d9f552..8fe94181a 100644 --- a/kube/src/config/file_config.rs +++ b/kube/src/config/file_config.rs @@ -69,6 +69,9 @@ pub struct Cluster { /// PEM-encoded certificate authority certificates. Overrides `certificate_authority` #[serde(rename = "certificate-authority-data")] pub certificate_authority_data: Option, + /// URL to the proxy to be used for all requests. + #[serde(rename = "proxy-url")] + pub proxy_url: Option, /// Additional information for extenders so that reads and writes don't clobber unknown fields pub extensions: Option>, } diff --git a/kube/src/config/file_loader.rs b/kube/src/config/file_loader.rs index fa6fa315f..8e0b59001 100644 --- a/kube/src/config/file_loader.rs +++ b/kube/src/config/file_loader.rs @@ -115,4 +115,17 @@ impl ConfigLoader { Ok(None) } } + + pub fn proxy_url(&self) -> Result> { + let nonempty = |o: Option| o.filter(|s| !s.is_empty()); + + if let Some(proxy) = nonempty(self.cluster.proxy_url.clone()) + .or_else(|| nonempty(std::env::var("HTTP_PROXY").ok())) + .or_else(|| nonempty(std::env::var("HTTPS_PROXY").ok())) + { + Ok(Some(proxy.parse::()?)) + } else { + Ok(None) + } + } } diff --git a/kube/src/config/mod.rs b/kube/src/config/mod.rs index 0520e9b20..519d02d4c 100644 --- a/kube/src/config/mod.rs +++ b/kube/src/config/mod.rs @@ -36,6 +36,9 @@ pub struct Config { pub(crate) identity_pem: Option>, /// Stores information to tell the cluster who you are. pub(crate) auth_info: AuthInfo, + // TODO Actually support proxy or create an example with custom client + /// Optional proxy URL. + pub proxy_url: Option, } impl Config { @@ -53,6 +56,7 @@ impl Config { accept_invalid_certs: false, identity_pem: None, auth_info: AuthInfo::default(), + proxy_url: None, } } @@ -113,6 +117,7 @@ impl Config { token: Some(token), ..Default::default() }, + proxy_url: None, }) } @@ -174,6 +179,7 @@ impl Config { timeout: Some(DEFAULT_TIMEOUT), accept_invalid_certs, identity_pem, + proxy_url: loader.proxy_url()?, auth_info: loader.user, }) } From 79096a991fc0728b5173cd8519c0b984cc15eee3 Mon Sep 17 00:00:00 2001 From: kazk Date: Fri, 4 Jun 2021 18:44:12 -0700 Subject: [PATCH 33/36] Add `ConfigExt::auth_layer` hiding details --- examples/custom_client.rs | 1 + kube/src/client/config_ext.rs | 30 +++++++++++++----------------- kube/src/client/middleware/mod.rs | 13 +++++++++++++ 3 files changed, 27 insertions(+), 17 deletions(-) diff --git a/examples/custom_client.rs b/examples/custom_client.rs index b035fa9d1..b464afb82 100644 --- a/examples/custom_client.rs +++ b/examples/custom_client.rs @@ -18,6 +18,7 @@ async fn main() -> anyhow::Result<()> { let client = Client::new( ServiceBuilder::new() .layer(config.base_uri_layer()) + .option_layer(config.auth_layer()?) .service(hyper::Client::builder().build(https)), ); diff --git a/kube/src/client/config_ext.rs b/kube/src/client/config_ext.rs index eac05bf7f..9c5d6f48f 100644 --- a/kube/src/client/config_ext.rs +++ b/kube/src/client/config_ext.rs @@ -5,7 +5,7 @@ use tower::util::Either; #[cfg(any(feature = "native-tls", feature = "rustls-tls"))] use super::tls; use super::{ auth::Auth, - middleware::{AddAuthorizationLayer, RefreshTokenLayer, SetBaseUriLayer}, + middleware::{AddAuthorizationLayer, AuthLayer, RefreshTokenLayer, SetBaseUriLayer}, }; use crate::{Config, Result}; @@ -16,6 +16,9 @@ pub trait ConfigExt: private::Sealed { /// Layer to set the base URI of requests to the configured server. fn base_uri_layer(&self) -> SetBaseUriLayer; + /// Optional layer to set up `Authorization` header depending on the config. + fn auth_layer(&self) -> Result>; + /// Create `native_tls::TlsConnector` #[cfg_attr(docsrs, doc(cfg(feature = "native-tls")))] #[cfg(feature = "native-tls")] @@ -35,13 +38,6 @@ pub trait ConfigExt: private::Sealed { #[cfg_attr(docsrs, doc(cfg(feature = "rustls-tls")))] #[cfg(feature = "rustls-tls")] fn rustls_https_connector(&self) -> Result>; - - // TODO Try reducing exported types to minimize API surface before making this public. - #[doc(hidden)] - /// Optional layer to set up `Authorization` header depending on the config. - /// - /// Users are not allowed to call this for now. - fn auth_layer(&self) -> Result>>; } mod private { @@ -54,6 +50,15 @@ impl ConfigExt for Config { SetBaseUriLayer::new(self.cluster_url.clone()) } + fn auth_layer(&self) -> Result> { + Ok(match Auth::try_from(&self.auth_info)? { + Auth::None => None, + Auth::Basic(user, pass) => Some(AuthLayer(Either::A(AddAuthorizationLayer::basic(&user, &pass)))), + Auth::Bearer(token) => Some(AuthLayer(Either::A(AddAuthorizationLayer::bearer(&token)))), + Auth::RefreshableToken(r) => Some(AuthLayer(Either::B(RefreshTokenLayer::new(r)))), + }) + } + #[cfg(feature = "native-tls")] fn native_tls_connector(&self) -> Result { tls::native_tls::native_tls_connector( @@ -87,13 +92,4 @@ impl ConfigExt for Config { http.enforce_http(false); Ok(hyper_rustls::HttpsConnector::from((http, rustls_config))) } - - fn auth_layer(&self) -> Result>> { - Ok(match Auth::try_from(&self.auth_info)? { - Auth::None => None, - Auth::Basic(user, pass) => Some(Either::A(AddAuthorizationLayer::basic(&user, &pass))), - Auth::Bearer(token) => Some(Either::A(AddAuthorizationLayer::bearer(&token))), - Auth::RefreshableToken(r) => Some(Either::B(RefreshTokenLayer::new(r))), - }) - } } diff --git a/kube/src/client/middleware/mod.rs b/kube/src/client/middleware/mod.rs index 1b88b16f5..dbcb02d16 100644 --- a/kube/src/client/middleware/mod.rs +++ b/kube/src/client/middleware/mod.rs @@ -1,4 +1,6 @@ //! Middleware types returned from `ConfigExt` methods. +use tower::{util::Either, Layer}; + mod add_authorization; mod base_uri; mod refresh_token; @@ -6,3 +8,14 @@ mod refresh_token; pub(crate) use add_authorization::AddAuthorizationLayer; pub use base_uri::{SetBaseUri, SetBaseUriLayer}; pub(crate) use refresh_token::RefreshTokenLayer; +/// Layer to set up `Authorization` header depending on the config. +pub struct AuthLayer(pub(crate) Either); + +impl Layer for AuthLayer { + type Service = + Either<>::Service, >::Service>; + + fn layer(&self, inner: S) -> Self::Service { + self.0.layer(inner) + } +} From 115353619b7d27904a5de8f294bb7d22b63670da Mon Sep 17 00:00:00 2001 From: kazk Date: Fri, 4 Jun 2021 22:54:24 -0700 Subject: [PATCH 34/36] Document custom client --- kube/Cargo.toml | 2 +- kube/src/client/config_ext.rs | 82 ++++++++++++++++++++++++++++++----- kube/src/client/mod.rs | 31 +++++++++++-- 3 files changed, 100 insertions(+), 15 deletions(-) diff --git a/kube/Cargo.toml b/kube/Cargo.toml index fede18b1c..c0d101bc0 100644 --- a/kube/Cargo.toml +++ b/kube/Cargo.toml @@ -32,7 +32,7 @@ config = ["__non_core", "pem", "dirs"] __non_core = ["tracing", "serde_yaml", "base64"] [package.metadata.docs.rs] -features = ["derive", "ws", "oauth", "jsonpatch", "admission"] +features = ["client", "native-tls", "rustls-tls", "derive", "ws", "oauth", "jsonpatch", "admission"] # Define the configuration attribute `docsrs`. Used to enable `doc_cfg` feature. rustdoc-args = ["--cfg", "docsrs"] diff --git a/kube/src/client/config_ext.rs b/kube/src/client/config_ext.rs index 9c5d6f48f..d47c89617 100644 --- a/kube/src/client/config_ext.rs +++ b/kube/src/client/config_ext.rs @@ -9,7 +9,9 @@ use super::{ }; use crate::{Config, Result}; -/// Extensions to `Config` for `Client`. +/// Extensions to [`Config`](crate::Config) for custom [`Client`](crate::Client). +/// +/// See [`Client::new`](crate::Client::new) for an example. /// /// This trait is sealed and cannot be implemented. pub trait ConfigExt: private::Sealed { @@ -19,25 +21,83 @@ pub trait ConfigExt: private::Sealed { /// Optional layer to set up `Authorization` header depending on the config. fn auth_layer(&self) -> Result>; - /// Create `native_tls::TlsConnector` - #[cfg_attr(docsrs, doc(cfg(feature = "native-tls")))] - #[cfg(feature = "native-tls")] - fn native_tls_connector(&self) -> Result; - - /// Create `hyper_tls::HttpsConnector` + /// Create [`hyper_tls::HttpsConnector`] based on config. + /// + /// # Example + /// + /// ```rust + /// # async fn doc() -> Result<(), Box> { + /// # use kube::{client::ConfigExt, Config}; + /// let config = Config::infer().await?; + /// let https = config.native_tls_https_connector()?; + /// let hyper_client: hyper::Client<_, hyper::Body> = hyper::Client::builder().build(https); + /// # Ok(()) + /// # } + /// ``` #[cfg_attr(docsrs, doc(cfg(feature = "native-tls")))] #[cfg(feature = "native-tls")] fn native_tls_https_connector(&self) -> Result>; - /// Create `rustls::ClientConfig` + /// Create [`hyper_rustls::HttpsConnector`] based on config. + /// + /// # Example + /// + /// ```rust + /// # async fn doc() -> Result<(), Box> { + /// # use kube::{client::ConfigExt, Config}; + /// let config = Config::infer().await?; + /// let https = config.rustls_https_connector()?; + /// let hyper_client: hyper::Client<_, hyper::Body> = hyper::Client::builder().build(https); + /// # Ok(()) + /// # } + /// ``` #[cfg_attr(docsrs, doc(cfg(feature = "rustls-tls")))] #[cfg(feature = "rustls-tls")] - fn rustls_client_config(&self) -> Result; + fn rustls_https_connector(&self) -> Result>; - /// Create `hyper_rustls::HttpsConnector` + /// Create [`native_tls::TlsConnector`](tokio_native_tls::native_tls::TlsConnector) based on config. + /// # Example + /// + /// ```rust + /// # async fn doc() -> Result<(), Box> { + /// # use hyper::client::HttpConnector; + /// # use kube::{client::ConfigExt, Client, Config}; + /// let config = Config::infer().await?; + /// let https = { + /// let tls = tokio_native_tls::TlsConnector::from( + /// config.native_tls_connector()? + /// ); + /// let mut http = HttpConnector::new(); + /// http.enforce_http(false); + /// hyper_tls::HttpsConnector::from((http, tls)) + /// }; + /// # Ok(()) + /// # } + /// ``` + #[cfg_attr(docsrs, doc(cfg(feature = "native-tls")))] + #[cfg(feature = "native-tls")] + fn native_tls_connector(&self) -> Result; + + /// Create [`rustls::ClientConfig`] based on config. + /// # Example + /// + /// ```rust + /// # async fn doc() -> Result<(), Box> { + /// # use hyper::client::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_https_connector(&self) -> Result>; + fn rustls_client_config(&self) -> Result; } mod private { diff --git a/kube/src/client/mod.rs b/kube/src/client/mod.rs index af03b56b9..fcd754b72 100644 --- a/kube/src/client/mod.rs +++ b/kube/src/client/mod.rs @@ -48,7 +48,7 @@ const WS_PROTOCOL: &str = "v4.channel.k8s.io"; /// Client for connecting with a Kubernetes cluster. /// -/// The best way to instantiate the client is either by +/// The easiest way to instantiate the client is either by /// inferring the configuration from the environment using /// [`Client::try_default`] or with an existing [`Config`] /// using [`Client::try_from`]. @@ -61,9 +61,34 @@ pub struct Client { } impl Client { - /// Create and initialize a [`Client`] using the given `Service`. + /// Create a [`Client`] using a custom `Service` stack. /// - /// Use [`Client::try_from`](Self::try_from) to create with a [`Config`]. + /// [`ConfigExt`](crate::client::ConfigExt) provides extensions for + /// building a custom stack. + /// + /// To create with the default stack with a [`Config`], use + /// [`Client::try_from`]. + /// + /// To create with the default stack with an inferred [`Config`], use + /// [`Client::try_default`]. + /// + /// # Example + /// + /// ```rust + /// # async fn doc() -> Result<(), Box> { + /// use kube::{client::ConfigExt, Client, Config}; + /// use tower::ServiceBuilder; + /// + /// let config = Config::infer().await?; + /// let client = Client::new( + /// ServiceBuilder::new() + /// .layer(config.base_uri_layer()) + /// .option_layer(config.auth_layer()?) + /// .service(hyper::Client::new()), + /// ); + /// # Ok(()) + /// # } + /// ``` pub fn new(service: S) -> Self where S: Service, Response = Response> + Send + 'static, From 8c540c0ccd44dfae490a05f3d8e87f2d3d3d212e Mon Sep 17 00:00:00 2001 From: kazk Date: Fri, 4 Jun 2021 23:45:08 -0700 Subject: [PATCH 35/36] Add missing feature tags --- kube-core/src/subresource.rs | 2 ++ kube/src/api/mod.rs | 6 +++++- kube/src/api/subresource.rs | 4 +++- kube/src/client/mod.rs | 1 + kube/src/config/mod.rs | 1 + kube/src/discovery/mod.rs | 1 + kube/src/error.rs | 3 +++ kube/src/lib.rs | 4 +++- 8 files changed, 19 insertions(+), 3 deletions(-) diff --git a/kube-core/src/subresource.rs b/kube-core/src/subresource.rs index ca343a0b9..a8284c91f 100644 --- a/kube-core/src/subresource.rs +++ b/kube-core/src/subresource.rs @@ -169,6 +169,7 @@ pub struct AttachParams { } #[cfg(feature = "ws")] +#[cfg_attr(docsrs, doc(cfg(feature = "ws")))] impl Default for AttachParams { // Default matching the server's defaults. fn default() -> Self { @@ -186,6 +187,7 @@ impl Default for AttachParams { } #[cfg(feature = "ws")] +#[cfg_attr(docsrs, doc(cfg(feature = "ws")))] impl AttachParams { /// Default parameters for an tty exec with stdin and stdout pub fn interactive_tty() -> Self { diff --git a/kube/src/api/mod.rs b/kube/src/api/mod.rs index 8f6a04e7f..385d73c2a 100644 --- a/kube/src/api/mod.rs +++ b/kube/src/api/mod.rs @@ -7,11 +7,14 @@ mod core_methods; mod subresource; #[cfg(feature = "ws")] +#[cfg_attr(docsrs, doc(cfg(feature = "ws")))] pub use subresource::{AttachParams, Attachable, Executable}; pub use subresource::{EvictParams, Evictable, LogParams, Loggable, ScaleSpec, ScaleStatus}; // Re-exports from kube-core -#[cfg(feature = "admission")] pub use kube_core::admission; +#[cfg(feature = "admission")] +#[cfg_attr(docsrs, doc(cfg(feature = "admission")))] +pub use kube_core::admission; pub(crate) use kube_core::params; pub use kube_core::{ dynamic::{ApiResource, DynamicObject}, @@ -32,6 +35,7 @@ use crate::Client; /// This abstracts over a [`Request`] and a type `K` so that /// we get automatic serialization/deserialization on the api calls /// implemented by the dynamic [`Resource`]. +#[cfg_attr(docsrs, doc(cfg(feature = "client")))] #[derive(Clone)] pub struct Api { /// The request builder object with its resource dependent url diff --git a/kube/src/api/subresource.rs b/kube/src/api/subresource.rs index 5913d9134..cb6915d25 100644 --- a/kube/src/api/subresource.rs +++ b/kube/src/api/subresource.rs @@ -11,7 +11,9 @@ use crate::{ use kube_core::response::Status; pub use kube_core::subresource::{EvictParams, LogParams}; -#[cfg(feature = "ws")] pub use kube_core::subresource::AttachParams; +#[cfg(feature = "ws")] +#[cfg_attr(docsrs, doc(cfg(feature = "ws")))] +pub use kube_core::subresource::AttachParams; pub use k8s_openapi::api::autoscaling::v1::{Scale, ScaleSpec, ScaleStatus}; diff --git a/kube/src/client/mod.rs b/kube/src/client/mod.rs index fcd754b72..a1a6e55f7 100644 --- a/kube/src/client/mod.rs +++ b/kube/src/client/mod.rs @@ -52,6 +52,7 @@ const WS_PROTOCOL: &str = "v4.channel.k8s.io"; /// inferring the configuration from the environment using /// [`Client::try_default`] or with an existing [`Config`] /// using [`Client::try_from`]. +#[cfg_attr(docsrs, doc(cfg(feature = "client")))] #[derive(Clone)] pub struct Client { // - `Buffer` for cheap clone diff --git a/kube/src/config/mod.rs b/kube/src/config/mod.rs index 519d02d4c..79babf5b5 100644 --- a/kube/src/config/mod.rs +++ b/kube/src/config/mod.rs @@ -17,6 +17,7 @@ pub use file_loader::KubeConfigOptions; use std::time::Duration; /// Configuration object detailing things like cluster URL, default namespace, root certificates, and timeouts. +#[cfg_attr(docsrs, doc(cfg(feature = "config")))] #[derive(Debug, Clone)] pub struct Config { /// The configured cluster url diff --git a/kube/src/discovery/mod.rs b/kube/src/discovery/mod.rs index 21e204ae5..fd51091f8 100644 --- a/kube/src/discovery/mod.rs +++ b/kube/src/discovery/mod.rs @@ -51,6 +51,7 @@ impl DiscoveryMode { /// If caching of results is __not required__, then a simpler [`oneshot`](crate::discovery::oneshot) discovery system can be used. /// /// [`ApiGroup`]: crate::discovery::ApiGroup +#[cfg_attr(docsrs, doc(cfg(feature = "client")))] pub struct Discovery { client: Client, groups: HashMap, diff --git a/kube/src/error.rs b/kube/src/error.rs index 7998992fa..b8346a87d 100644 --- a/kube/src/error.rs +++ b/kube/src/error.rs @@ -5,6 +5,7 @@ use std::path::PathBuf; use thiserror::Error; /// Possible errors when working with [`kube`][crate] +#[cfg_attr(docsrs, doc(cfg(any(feature = "config", feature = "client"))))] #[derive(Error, Debug)] pub enum Error { /// ApiError for when things fail @@ -85,6 +86,7 @@ pub enum Error { /// An error from openssl when handling configuration #[cfg(feature = "native-tls")] + #[cfg_attr(docsrs, doc(cfg(feature = "native-tls")))] #[error("OpensslError: {0}")] OpensslError(#[from] openssl::error::ErrorStack), @@ -170,6 +172,7 @@ pub enum ConfigError { ExecPluginFailed, #[cfg(feature = "client")] + #[cfg_attr(docsrs, doc(cfg(feature = "client")))] #[error("Malformed token expiration date: {0}")] MalformedTokenExpirationDate(#[source] chrono::ParseError), diff --git a/kube/src/lib.rs b/kube/src/lib.rs index bd0cec915..9b39df0b6 100644 --- a/kube/src/lib.rs +++ b/kube/src/lib.rs @@ -135,7 +135,9 @@ pub use kube_derive::CustomResource; /// Re-exports from kube_core crate. pub mod core { - #[cfg(feature = "admission")] pub use kube_core::admission; + #[cfg(feature = "admission")] + #[cfg_attr(docsrs, doc(cfg(feature = "admission")))] + pub use kube_core::admission; pub use kube_core::{ dynamic::{self, ApiResource, DynamicObject}, gvk::{self, GroupVersionKind, GroupVersionResource}, From 38782b52a1f55a9540f05732023de6ef162a4053 Mon Sep 17 00:00:00 2001 From: kazk Date: Sat, 5 Jun 2021 13:56:22 -0700 Subject: [PATCH 36/36] Rename to `BaseUriLayer` --- kube/src/client/config_ext.rs | 8 ++++---- kube/src/client/middleware/base_uri.rs | 18 +++++++++--------- kube/src/client/middleware/mod.rs | 2 +- 3 files changed, 14 insertions(+), 14 deletions(-) diff --git a/kube/src/client/config_ext.rs b/kube/src/client/config_ext.rs index d47c89617..490532d7d 100644 --- a/kube/src/client/config_ext.rs +++ b/kube/src/client/config_ext.rs @@ -5,7 +5,7 @@ use tower::util::Either; #[cfg(any(feature = "native-tls", feature = "rustls-tls"))] use super::tls; use super::{ auth::Auth, - middleware::{AddAuthorizationLayer, AuthLayer, RefreshTokenLayer, SetBaseUriLayer}, + middleware::{AddAuthorizationLayer, AuthLayer, BaseUriLayer, RefreshTokenLayer}, }; use crate::{Config, Result}; @@ -16,7 +16,7 @@ use crate::{Config, Result}; /// This trait is sealed and cannot be implemented. pub trait ConfigExt: private::Sealed { /// Layer to set the base URI of requests to the configured server. - fn base_uri_layer(&self) -> SetBaseUriLayer; + fn base_uri_layer(&self) -> BaseUriLayer; /// Optional layer to set up `Authorization` header depending on the config. fn auth_layer(&self) -> Result>; @@ -106,8 +106,8 @@ mod private { } impl ConfigExt for Config { - fn base_uri_layer(&self) -> SetBaseUriLayer { - SetBaseUriLayer::new(self.cluster_url.clone()) + fn base_uri_layer(&self) -> BaseUriLayer { + BaseUriLayer::new(self.cluster_url.clone()) } fn auth_layer(&self) -> Result> { diff --git a/kube/src/client/middleware/base_uri.rs b/kube/src/client/middleware/base_uri.rs index 545ff8c51..f2c296e0e 100644 --- a/kube/src/client/middleware/base_uri.rs +++ b/kube/src/client/middleware/base_uri.rs @@ -2,26 +2,26 @@ use http::{uri, Request}; use tower::{Layer, Service}; -/// Layer that applies [`SetBaseUri`] which makes all requests relative to the base URI. +/// Layer that applies [`BaseUri`] which makes all requests relative to the URI. /// -/// Path in `base_uri` is preseved. +/// Path in the base URI is preseved. #[derive(Debug, Clone)] -pub struct SetBaseUriLayer { +pub struct BaseUriLayer { base_uri: http::Uri, } -impl SetBaseUriLayer { +impl BaseUriLayer { /// Set base URI of requests. pub fn new(base_uri: http::Uri) -> Self { Self { base_uri } } } -impl Layer for SetBaseUriLayer { - type Service = SetBaseUri; +impl Layer for BaseUriLayer { + type Service = BaseUri; fn layer(&self, inner: S) -> Self::Service { - SetBaseUri { + BaseUri { base_uri: self.base_uri.clone(), inner, } @@ -30,12 +30,12 @@ impl Layer for SetBaseUriLayer { /// Middleware that sets base URI so that all requests are relative to it. #[derive(Debug, Clone)] -pub struct SetBaseUri { +pub struct BaseUri { base_uri: http::Uri, inner: S, } -impl Service> for SetBaseUri +impl Service> for BaseUri where S: Service>, { diff --git a/kube/src/client/middleware/mod.rs b/kube/src/client/middleware/mod.rs index dbcb02d16..667c140c1 100644 --- a/kube/src/client/middleware/mod.rs +++ b/kube/src/client/middleware/mod.rs @@ -6,7 +6,7 @@ mod base_uri; mod refresh_token; pub(crate) use add_authorization::AddAuthorizationLayer; -pub use base_uri::{SetBaseUri, SetBaseUriLayer}; +pub use base_uri::{BaseUri, BaseUriLayer}; pub(crate) use refresh_token::RefreshTokenLayer; /// Layer to set up `Authorization` header depending on the config. pub struct AuthLayer(pub(crate) Either);