From 1b313ab8065e0d64c92175e58b30dd9e6fce4eca Mon Sep 17 00:00:00 2001 From: clux Date: Mon, 18 Dec 2023 06:28:20 +0000 Subject: [PATCH 01/16] MVP Client Extensions AKA Api calls without `Api`. To avoid overreaching in this PR we only deal with `.get` and `.list` for cluster and namespace scoped resources. An example node_watcher uses this (to show we can avoid the clone and api construction) for `.list_all`. Signed-off-by: clux --- examples/Cargo.toml | 4 +- examples/node_watcher.rs | 7 +- kube-client/Cargo.toml | 1 + kube-client/src/client/client_ext.rs | 141 +++++++++++++++++++++++++++ kube-client/src/client/mod.rs | 1 + kube/Cargo.toml | 1 + 6 files changed, 149 insertions(+), 6 deletions(-) create mode 100644 kube-client/src/client/client_ext.rs diff --git a/examples/Cargo.toml b/examples/Cargo.toml index 8701d864a..7281d4377 100644 --- a/examples/Cargo.toml +++ b/examples/Cargo.toml @@ -16,8 +16,8 @@ release = false [features] default = ["rustls-tls", "kubederive", "ws", "latest", "socks5", "runtime", "refresh"] kubederive = ["kube/derive"] -openssl-tls = ["kube/client", "kube/openssl-tls"] -rustls-tls = ["kube/client", "kube/rustls-tls"] +openssl-tls = ["kube/client", "kube/openssl-tls", "kube/unstable-client"] +rustls-tls = ["kube/client", "kube/rustls-tls", "kube/unstable-client"] runtime = ["kube/runtime", "kube/unstable-runtime"] socks5 = ["kube/socks5"] refresh = ["kube/oauth", "kube/oidc"] diff --git a/examples/node_watcher.rs b/examples/node_watcher.rs index 1391abe11..42054cc64 100644 --- a/examples/node_watcher.rs +++ b/examples/node_watcher.rs @@ -11,7 +11,6 @@ use tracing::*; async fn main() -> anyhow::Result<()> { tracing_subscriber::fmt::init(); let client = Client::try_default().await?; - let events: Api = Api::all(client.clone()); let nodes: Api = Api::all(client.clone()); let use_watchlist = std::env::var("WATCHLIST").map(|s| s == "1").unwrap_or(false); @@ -25,13 +24,13 @@ async fn main() -> anyhow::Result<()> { pin_mut!(obs); while let Some(n) = obs.try_next().await? { - check_for_node_failures(&events, n).await?; + check_for_node_failures(&client, n).await?; } Ok(()) } // A simple node problem detector -async fn check_for_node_failures(events: &Api, o: Node) -> anyhow::Result<()> { +async fn check_for_node_failures(client: &Client, o: Node) -> anyhow::Result<()> { let name = o.name_any(); // Nodes often modify a lot - only print broken nodes if let Some(true) = o.spec.unwrap().unschedulable { @@ -52,7 +51,7 @@ async fn check_for_node_failures(events: &Api, o: Node) -> anyhow::Result // Find events related to this node let opts = ListParams::default().fields(&format!("involvedObject.kind=Node,involvedObject.name={name}")); - let evlist = events.list(&opts).await?; + let evlist = client.list_all::(&opts).await?; for e in evlist { warn!("Node event: {:?}", serde_json::to_string_pretty(&e)?); } diff --git a/kube-client/Cargo.toml b/kube-client/Cargo.toml index 9ed10cbf7..f82f0d312 100644 --- a/kube-client/Cargo.toml +++ b/kube-client/Cargo.toml @@ -28,6 +28,7 @@ jsonpatch = ["kube-core/jsonpatch"] admission = ["kube-core/admission"] config = ["__non_core", "pem", "home"] socks5 = ["hyper-socks2"] +unstable-client = [] # private feature sets; do not use __non_core = ["tracing", "serde_yaml", "base64"] diff --git a/kube-client/src/client/client_ext.rs b/kube-client/src/client/client_ext.rs new file mode 100644 index 000000000..c9c974367 --- /dev/null +++ b/kube-client/src/client/client_ext.rs @@ -0,0 +1,141 @@ +use crate::{Client, Error, Result}; +use kube_core::{ + object::ObjectList, + params::{GetParams, ListParams}, + request::Request, + ClusterResourceScope, NamespaceResourceScope, Resource, +}; +use serde::{de::DeserializeOwned, Serialize}; +use std::fmt::Debug; + +use k8s_openapi::api::core::v1::Namespace as k8sNs; + +/// Convenience newtype for a namespace +pub struct Namespace(String); + +// can be created from a complete native object +impl TryFrom<&k8sNs> for Namespace { + type Error = NamespaceError; + + fn try_from(ns: &k8sNs) -> Result { + if let Some(n) = &ns.meta().name { + Ok(Namespace(n.to_owned())) + } else { + Err(NamespaceError::MissingName) + } + } +} +// and from literals + owned strings +impl From<&str> for Namespace { + fn from(ns: &str) -> Namespace { + Namespace(ns.to_owned()) + } +} +impl From for Namespace { + fn from(ns: String) -> Namespace { + Namespace(ns) + } +} + +#[derive(thiserror::Error, Debug)] +/// Failures to infer a namespace +pub enum NamespaceError { + /// MissingName + #[error("Missing Namespace Name")] + MissingName, +} + +// helper constructors for the Request object +fn namespaced_request(ns: &Namespace) -> Request +where + K: Resource, + ::DynamicType: Default, +{ + let url = K::url_path(&K::DynamicType::default(), Some(&ns.0)); + Request::new(url) +} +fn global_request() -> Request +where + K: Resource, + ::DynamicType: Default, +{ + let url = K::url_path(&K::DynamicType::default(), None); + Request::new(url) +} +fn cluster_request() -> Request +where + K: Resource, + ::DynamicType: Default, +{ + let url = K::url_path(&K::DynamicType::default(), None); + Request::new(url) +} + +impl Client { + async fn get_raw(&self, r: Request, gp: &GetParams, name: &str) -> Result + where + K: Resource + Serialize + DeserializeOwned + Clone + Debug, + { + let mut req = r.get(name, gp).map_err(Error::BuildRequest)?; + req.extensions_mut().insert("get"); + self.request::(req).await + } + + async fn list_raw(&self, r: Request, lp: &ListParams) -> Result> + where + K: Resource + DeserializeOwned + Clone, + { + let mut req = r.list(lp).map_err(Error::BuildRequest)?; + req.extensions_mut().insert("list"); + self.request::>(req).await + } +} + +/// Methods for NamespaceResourceScope Resource implementors +impl Client { + /// Get a namespaced resource + pub async fn get_namespaced(&self, name: &str, ns: &Namespace) -> Result + where + K: Resource + Serialize + DeserializeOwned + Clone + Debug, + ::DynamicType: Default, + { + let request = namespaced_request::(ns); + self.get_raw(request, &GetParams::default(), name).await + } + + /// List a namespaced resource + pub async fn list_namespaced(&self, lp: &ListParams, ns: &Namespace) -> Result> + where + K: Resource + Serialize + DeserializeOwned + Clone + Debug, + ::DynamicType: Default, + { + let request = namespaced_request::(ns); + self.list_raw(request, lp).await + } + + /// Get a namespaced resource + pub async fn get(&self, name: &str) -> Result + where + K: Resource + Serialize + DeserializeOwned + Clone + Debug, + ::DynamicType: Default, + { + let request = cluster_request::(); + self.get_raw(request, &GetParams::default(), name).await + } + + /// List a namespaced resource across namespaces + pub async fn list_all(&self, lp: &ListParams) -> Result> + where + K: Resource + Serialize + DeserializeOwned + Clone + Debug, + ::DynamicType: Default, + { + let request = global_request::(); + self.list_raw(request, lp).await + } + + /// Convenience helper to list namespaces + pub async fn list_available_namespaces(&self, lp: &ListParams) -> Result> { + let request = cluster_request::(); + self.list_raw(request, lp).await + } +} diff --git a/kube-client/src/client/mod.rs b/kube-client/src/client/mod.rs index 83d87e74a..95edb512d 100644 --- a/kube-client/src/client/mod.rs +++ b/kube-client/src/client/mod.rs @@ -31,6 +31,7 @@ mod body; mod builder; // Add `into_stream()` to `http::Body` use body::BodyStreamExt; +#[cfg(any(feature = "unstable-client"))] mod client_ext; mod config_ext; pub use auth::Error as AuthError; pub use config_ext::ConfigExt; diff --git a/kube/Cargo.toml b/kube/Cargo.toml index c8448db96..6265e83c2 100644 --- a/kube/Cargo.toml +++ b/kube/Cargo.toml @@ -36,6 +36,7 @@ admission = ["kube-core/admission"] derive = ["kube-derive", "kube-core/schema"] runtime = ["kube-runtime"] unstable-runtime = ["kube-runtime/unstable-runtime"] +unstable-client = ["kube-client/unstable-client"] socks5 = ["kube-client/socks5"] [package.metadata.docs.rs] From a358d4f649c8a05ff752837afbd2a5fa71a1e7ad Mon Sep 17 00:00:00 2001 From: clux Date: Mon, 18 Dec 2023 06:57:48 +0000 Subject: [PATCH 02/16] one line doc fixes + basic integration test Signed-off-by: clux --- kube-client/src/client/client_ext.rs | 26 ++++++++++++++++++++++++-- 1 file changed, 24 insertions(+), 2 deletions(-) diff --git a/kube-client/src/client/client_ext.rs b/kube-client/src/client/client_ext.rs index c9c974367..cb6e69277 100644 --- a/kube-client/src/client/client_ext.rs +++ b/kube-client/src/client/client_ext.rs @@ -91,7 +91,7 @@ impl Client { } } -/// Methods for NamespaceResourceScope Resource implementors +/// Client extensions to allow typed api calls without [`Api`] impl Client { /// Get a namespaced resource pub async fn get_namespaced(&self, name: &str, ns: &Namespace) -> Result @@ -113,7 +113,7 @@ impl Client { self.list_raw(request, lp).await } - /// Get a namespaced resource + /// Get a cluster scoped resource pub async fn get(&self, name: &str) -> Result where K: Resource + Serialize + DeserializeOwned + Clone + Debug, @@ -139,3 +139,25 @@ impl Client { self.list_raw(request, lp).await } } + +#[cfg(test)] +#[cfg(feature = "client")] +mod test { + use super::{Client, ListParams}; + use kube_core::ResourceExt; + + #[tokio::test] + #[ignore = "needs cluster (will list namespaces)"] + async fn list_pods_across_namespaces() -> Result<(), Box> { + use k8s_openapi::api::core::v1::Pod; + + let client = Client::try_default().await?; + let lp = ListParams::default(); + for ns in client.list_available_namespaces(&lp).await? { + for p in client.list_namespaced::(&lp, &(&ns).try_into()?).await? { + println!("Found pod {} in {}", p.name_any(), ns.name_any()); + } + } + Ok(()) + } +} From 32a7a27b739b211167474d53fc6504f682718a11 Mon Sep 17 00:00:00 2001 From: clux Date: Mon, 18 Dec 2023 07:41:03 +0000 Subject: [PATCH 03/16] fix lint + add missing cluster list + remove unnecessary convenience Signed-off-by: clux --- kube-client/src/client/client_ext.rs | 20 ++++++++++++-------- kube-client/src/client/mod.rs | 2 +- 2 files changed, 13 insertions(+), 9 deletions(-) diff --git a/kube-client/src/client/client_ext.rs b/kube-client/src/client/client_ext.rs index cb6e69277..b901c5766 100644 --- a/kube-client/src/client/client_ext.rs +++ b/kube-client/src/client/client_ext.rs @@ -113,6 +113,16 @@ impl Client { self.list_raw(request, lp).await } + /// List a cluster resource + pub async fn list(&self, lp: &ListParams) -> Result> + where + K: Resource + Serialize + DeserializeOwned + Clone + Debug, + ::DynamicType: Default, + { + let request = cluster_request::(); + self.list_raw(request, lp).await + } + /// Get a cluster scoped resource pub async fn get(&self, name: &str) -> Result where @@ -132,12 +142,6 @@ impl Client { let request = global_request::(); self.list_raw(request, lp).await } - - /// Convenience helper to list namespaces - pub async fn list_available_namespaces(&self, lp: &ListParams) -> Result> { - let request = cluster_request::(); - self.list_raw(request, lp).await - } } #[cfg(test)] @@ -149,11 +153,11 @@ mod test { #[tokio::test] #[ignore = "needs cluster (will list namespaces)"] async fn list_pods_across_namespaces() -> Result<(), Box> { - use k8s_openapi::api::core::v1::Pod; + use k8s_openapi::api::core::v1::{Namespace as k8sNs, Pod}; let client = Client::try_default().await?; let lp = ListParams::default(); - for ns in client.list_available_namespaces(&lp).await? { + for ns in client.list::(&lp).await? { for p in client.list_namespaced::(&lp, &(&ns).try_into()?).await? { println!("Found pod {} in {}", p.name_any(), ns.name_any()); } diff --git a/kube-client/src/client/mod.rs b/kube-client/src/client/mod.rs index 95edb512d..ee6ef0739 100644 --- a/kube-client/src/client/mod.rs +++ b/kube-client/src/client/mod.rs @@ -31,7 +31,7 @@ mod body; mod builder; // Add `into_stream()` to `http::Body` use body::BodyStreamExt; -#[cfg(any(feature = "unstable-client"))] mod client_ext; +#[cfg(feature = "unstable-client")] mod client_ext; mod config_ext; pub use auth::Error as AuthError; pub use config_ext::ConfigExt; From f2d3311268c851bc92c5b42fb3accfaa5337831d Mon Sep 17 00:00:00 2001 From: clux Date: Mon, 18 Dec 2023 07:43:37 +0000 Subject: [PATCH 04/16] reorder methods so they are listed in order of verb first Signed-off-by: clux --- kube-client/src/client/client_ext.rs | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/kube-client/src/client/client_ext.rs b/kube-client/src/client/client_ext.rs index b901c5766..053595086 100644 --- a/kube-client/src/client/client_ext.rs +++ b/kube-client/src/client/client_ext.rs @@ -93,24 +93,24 @@ impl Client { /// Client extensions to allow typed api calls without [`Api`] impl Client { - /// Get a namespaced resource - pub async fn get_namespaced(&self, name: &str, ns: &Namespace) -> Result + /// Get a cluster scoped resource + pub async fn get(&self, name: &str) -> Result where - K: Resource + Serialize + DeserializeOwned + Clone + Debug, + K: Resource + Serialize + DeserializeOwned + Clone + Debug, ::DynamicType: Default, { - let request = namespaced_request::(ns); + let request = cluster_request::(); self.get_raw(request, &GetParams::default(), name).await } - /// List a namespaced resource - pub async fn list_namespaced(&self, lp: &ListParams, ns: &Namespace) -> Result> + /// Get a namespaced resource + pub async fn get_namespaced(&self, name: &str, ns: &Namespace) -> Result where K: Resource + Serialize + DeserializeOwned + Clone + Debug, ::DynamicType: Default, { let request = namespaced_request::(ns); - self.list_raw(request, lp).await + self.get_raw(request, &GetParams::default(), name).await } /// List a cluster resource @@ -123,14 +123,14 @@ impl Client { self.list_raw(request, lp).await } - /// Get a cluster scoped resource - pub async fn get(&self, name: &str) -> Result + /// List a namespaced resource + pub async fn list_namespaced(&self, lp: &ListParams, ns: &Namespace) -> Result> where - K: Resource + Serialize + DeserializeOwned + Clone + Debug, + K: Resource + Serialize + DeserializeOwned + Clone + Debug, ::DynamicType: Default, { - let request = cluster_request::(); - self.get_raw(request, &GetParams::default(), name).await + let request = namespaced_request::(ns); + self.list_raw(request, lp).await } /// List a namespaced resource across namespaces From e6d7b36649f1cc516f83cdd65a0e0ee8b4d931d7 Mon Sep 17 00:00:00 2001 From: clux Date: Fri, 5 Jan 2024 07:05:19 +0000 Subject: [PATCH 05/16] add docs and more tests Signed-off-by: clux --- kube-client/src/client/client_ext.rs | 89 ++++++++++++++++++++++++++-- 1 file changed, 85 insertions(+), 4 deletions(-) diff --git a/kube-client/src/client/client_ext.rs b/kube-client/src/client/client_ext.rs index 053595086..ac70f8953 100644 --- a/kube-client/src/client/client_ext.rs +++ b/kube-client/src/client/client_ext.rs @@ -94,6 +94,17 @@ impl Client { /// Client extensions to allow typed api calls without [`Api`] impl Client { /// Get a cluster scoped resource + /// + /// ```no_run + /// # use k8s_openapi::api::rbac::v1::ClusterRole; + /// # use kube::{ResourceExt, api::GetParams}; + /// # async fn wrapper() -> Result<(), Box> { + /// # let client: kube::Client = todo!(); + /// let crole = client.get::("cluster-admin").await?; + /// assert_eq!(crole.name_unchecked(), "cluster-admin"); + /// # Ok(()) + /// # } + /// ``` pub async fn get(&self, name: &str) -> Result where K: Resource + Serialize + DeserializeOwned + Clone + Debug, @@ -104,6 +115,18 @@ impl Client { } /// Get a namespaced resource + /// + /// ```no_run + /// # use k8s_openapi::api::core::v1::Service; + /// # use kube::{ResourceExt, api::GetParams}; + /// # async fn wrapper() -> Result<(), Box> { + /// # let client: kube::Client = todo!(); + /// let ns = "default".try_into()?; + /// let svc = client.get_namespaced::("kubernetes", &ns).await?; + /// assert_eq!(svc.name_unchecked(), "kubernetes"); + /// # Ok(()) + /// # } + /// ``` pub async fn get_namespaced(&self, name: &str, ns: &Namespace) -> Result where K: Resource + Serialize + DeserializeOwned + Clone + Debug, @@ -114,6 +137,19 @@ impl Client { } /// List a cluster resource + /// + /// ```no_run + /// # use k8s_openapi::api::rbac::v1::ClusterRole; + /// # use kube::{ResourceExt, api::ListParams}; + /// # async fn wrapper() -> Result<(), Box> { + /// # let client: kube::Client = todo!(); + /// let lp = ListParams::default(); + /// for svc in client.list::(&lp).await? { + /// println!("Found clusterrole {}", svc.name_any()); + /// } + /// # Ok(()) + /// # } + /// ``` pub async fn list(&self, lp: &ListParams) -> Result> where K: Resource + Serialize + DeserializeOwned + Clone + Debug, @@ -124,6 +160,20 @@ impl Client { } /// List a namespaced resource + /// + /// ```no_run + /// # use k8s_openapi::api::core::v1::Service; + /// # use kube::{ResourceExt, api::ListParams}; + /// # async fn wrapper() -> Result<(), Box> { + /// # let client: kube::Client = todo!(); + /// let lp = ListParams::default(); + /// let ns = "default".try_into()?; + /// for svc in client.list_namespaced::(&lp, &ns).await? { + /// println!("Found service {}", svc.name_any()); + /// } + /// # Ok(()) + /// # } + /// ``` pub async fn list_namespaced(&self, lp: &ListParams, ns: &Namespace) -> Result> where K: Resource + Serialize + DeserializeOwned + Clone + Debug, @@ -134,6 +184,19 @@ impl Client { } /// List a namespaced resource across namespaces + /// + /// ```no_run + /// # use k8s_openapi::api::batch::v1::Job; + /// # use kube::{ResourceExt, api::ListParams}; + /// # async fn wrapper() -> Result<(), Box> { + /// # let client: kube::Client = todo!(); + /// let lp = ListParams::default(); + /// for j in client.list_all::(&lp).await? { + /// println!("Found job {} in {}", j.name_any(), j.namespace().unwrap()); + /// } + /// # Ok(()) + /// # } + /// ``` pub async fn list_all(&self, lp: &ListParams) -> Result> where K: Resource + Serialize + DeserializeOwned + Clone + Debug, @@ -147,21 +210,39 @@ impl Client { #[cfg(test)] #[cfg(feature = "client")] mod test { - use super::{Client, ListParams}; + use super::{Client, ListParams, Namespace}; use kube_core::ResourceExt; #[tokio::test] - #[ignore = "needs cluster (will list namespaces)"] - async fn list_pods_across_namespaces() -> Result<(), Box> { - use k8s_openapi::api::core::v1::{Namespace as k8sNs, Pod}; + #[ignore = "needs cluster (will list/get namespaces, pods, jobs, svcs, clusterroles)"] + async fn client_ext_list_get_pods_svcs() -> Result<(), Box> { + use k8s_openapi::api::{ + batch::v1::Job, + core::v1::{Namespace as k8sNs, Pod, Service}, + rbac::v1::ClusterRole, + }; let client = Client::try_default().await?; let lp = ListParams::default(); + // cluster-scoped list for ns in client.list::(&lp).await? { + // namespaced list for p in client.list_namespaced::(&lp, &(&ns).try_into()?).await? { println!("Found pod {} in {}", p.name_any(), ns.name_any()); } } + // across-namespace list + for j in client.list_all::(&lp).await? { + println!("Found job {} in {}", j.name_any(), j.namespace().unwrap()); + } + // namespaced get + let default: Namespace = "default".try_into()?; + let svc = client.get_namespaced::("kubernetes", &default).await?; + assert_eq!(svc.name_unchecked(), "kubernetes"); + // global get + let ca = client.get::("cluster-admin").await?; + assert_eq!(ca.name_unchecked(), "cluster-admin"); + Ok(()) } } From c011954c4e453566a2a30f17d50e1dd75d410efb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Natalie=20Klestrup=20R=C3=B6ijezon?= Date: Thu, 29 Feb 2024 15:45:40 +0100 Subject: [PATCH 06/16] Move request scope into a separate parameter --- kube-client/src/client/client_ext.rs | 210 ++++++++++----------------- 1 file changed, 79 insertions(+), 131 deletions(-) diff --git a/kube-client/src/client/client_ext.rs b/kube-client/src/client/client_ext.rs index ac70f8953..0d4595167 100644 --- a/kube-client/src/client/client_ext.rs +++ b/kube-client/src/client/client_ext.rs @@ -10,9 +10,61 @@ use std::fmt::Debug; use k8s_openapi::api::core::v1::Namespace as k8sNs; -/// Convenience newtype for a namespace +pub trait ListScope { + fn url_path(&self) -> String; +} + +pub trait ObjectScope { + fn url_path(&self) -> String; +} + +pub struct Cluster; + +// All objects can be listed cluster-wide +impl ListScope for Cluster +where + K: Resource, + K::DynamicType: Default, +{ + fn url_path(&self) -> String { + K::url_path(&K::DynamicType::default(), None) + } +} + +// Only cluster-scoped objects can be named globally +impl ObjectScope for Cluster +where + K: Resource, + K::DynamicType: Default, +{ + fn url_path(&self) -> String { + K::url_path(&K::DynamicType::default(), None) + } +} + pub struct Namespace(String); +// Only namespaced objects can be accessed via namespace +impl ListScope for Namespace +where + K: Resource, + K::DynamicType: Default, +{ + fn url_path(&self) -> String { + K::url_path(&K::DynamicType::default(), Some(&self.0)) + } +} + +impl ObjectScope for Namespace +where + K: Resource, + K::DynamicType: Default, +{ + fn url_path(&self) -> String { + K::url_path(&K::DynamicType::default(), Some(&self.0)) + } +} + // can be created from a complete native object impl TryFrom<&k8sNs> for Namespace { type Error = NamespaceError; @@ -45,98 +97,35 @@ pub enum NamespaceError { MissingName, } -// helper constructors for the Request object -fn namespaced_request(ns: &Namespace) -> Request -where - K: Resource, - ::DynamicType: Default, -{ - let url = K::url_path(&K::DynamicType::default(), Some(&ns.0)); - Request::new(url) -} -fn global_request() -> Request -where - K: Resource, - ::DynamicType: Default, -{ - let url = K::url_path(&K::DynamicType::default(), None); - Request::new(url) -} -fn cluster_request() -> Request -where - K: Resource, - ::DynamicType: Default, -{ - let url = K::url_path(&K::DynamicType::default(), None); - Request::new(url) -} - -impl Client { - async fn get_raw(&self, r: Request, gp: &GetParams, name: &str) -> Result - where - K: Resource + Serialize + DeserializeOwned + Clone + Debug, - { - let mut req = r.get(name, gp).map_err(Error::BuildRequest)?; - req.extensions_mut().insert("get"); - self.request::(req).await - } - - async fn list_raw(&self, r: Request, lp: &ListParams) -> Result> - where - K: Resource + DeserializeOwned + Clone, - { - let mut req = r.list(lp).map_err(Error::BuildRequest)?; - req.extensions_mut().insert("list"); - self.request::>(req).await - } -} - /// Client extensions to allow typed api calls without [`Api`] impl Client { - /// Get a cluster scoped resource + /// Get a resource /// /// ```no_run /// # use k8s_openapi::api::rbac::v1::ClusterRole; /// # use kube::{ResourceExt, api::GetParams}; /// # async fn wrapper() -> Result<(), Box> { /// # let client: kube::Client = todo!(); - /// let crole = client.get::("cluster-admin").await?; + /// let crole = client.get::("cluster-admin", &Cluster).await?; /// assert_eq!(crole.name_unchecked(), "cluster-admin"); - /// # Ok(()) - /// # } - /// ``` - pub async fn get(&self, name: &str) -> Result - where - K: Resource + Serialize + DeserializeOwned + Clone + Debug, - ::DynamicType: Default, - { - let request = cluster_request::(); - self.get_raw(request, &GetParams::default(), name).await - } - - /// Get a namespaced resource - /// - /// ```no_run - /// # use k8s_openapi::api::core::v1::Service; - /// # use kube::{ResourceExt, api::GetParams}; - /// # async fn wrapper() -> Result<(), Box> { - /// # let client: kube::Client = todo!(); - /// let ns = "default".try_into()?; - /// let svc = client.get_namespaced::("kubernetes", &ns).await?; + /// let svc = client.get::("kubernetes", &Namespace::from("default")).await?; /// assert_eq!(svc.name_unchecked(), "kubernetes"); /// # Ok(()) /// # } /// ``` - pub async fn get_namespaced(&self, name: &str, ns: &Namespace) -> Result + pub async fn get(&self, name: &str, scope: &impl ObjectScope) -> Result where - K: Resource + Serialize + DeserializeOwned + Clone + Debug, + K: Resource + Serialize + DeserializeOwned + Clone + Debug, ::DynamicType: Default, { - let request = namespaced_request::(ns); - self.get_raw(request, &GetParams::default(), name).await + let mut req = Request::new(scope.url_path()) + .get(name, &GetParams::default()) + .map_err(Error::BuildRequest)?; + req.extensions_mut().insert("get"); + self.request::(req).await } - /// List a cluster resource + /// List a resource /// /// ```no_run /// # use k8s_openapi::api::rbac::v1::ClusterRole; @@ -147,70 +136,29 @@ impl Client { /// for svc in client.list::(&lp).await? { /// println!("Found clusterrole {}", svc.name_any()); /// } - /// # Ok(()) - /// # } - /// ``` - pub async fn list(&self, lp: &ListParams) -> Result> - where - K: Resource + Serialize + DeserializeOwned + Clone + Debug, - ::DynamicType: Default, - { - let request = cluster_request::(); - self.list_raw(request, lp).await - } - - /// List a namespaced resource - /// - /// ```no_run - /// # use k8s_openapi::api::core::v1::Service; - /// # use kube::{ResourceExt, api::ListParams}; - /// # async fn wrapper() -> Result<(), Box> { - /// # let client: kube::Client = todo!(); - /// let lp = ListParams::default(); - /// let ns = "default".try_into()?; - /// for svc in client.list_namespaced::(&lp, &ns).await? { + /// for svc in client.list::(&lp, &Namespace::from("default")).await? { /// println!("Found service {}", svc.name_any()); /// } /// # Ok(()) /// # } /// ``` - pub async fn list_namespaced(&self, lp: &ListParams, ns: &Namespace) -> Result> - where - K: Resource + Serialize + DeserializeOwned + Clone + Debug, - ::DynamicType: Default, - { - let request = namespaced_request::(ns); - self.list_raw(request, lp).await - } - - /// List a namespaced resource across namespaces - /// - /// ```no_run - /// # use k8s_openapi::api::batch::v1::Job; - /// # use kube::{ResourceExt, api::ListParams}; - /// # async fn wrapper() -> Result<(), Box> { - /// # let client: kube::Client = todo!(); - /// let lp = ListParams::default(); - /// for j in client.list_all::(&lp).await? { - /// println!("Found job {} in {}", j.name_any(), j.namespace().unwrap()); - /// } - /// # Ok(()) - /// # } - /// ``` - pub async fn list_all(&self, lp: &ListParams) -> Result> + pub async fn list(&self, lp: &ListParams, scope: &impl ListScope) -> Result> where - K: Resource + Serialize + DeserializeOwned + Clone + Debug, + K: Resource + Serialize + DeserializeOwned + Clone + Debug, ::DynamicType: Default, { - let request = global_request::(); - self.list_raw(request, lp).await + let mut req = Request::new(scope.url_path()) + .list(lp) + .map_err(Error::BuildRequest)?; + req.extensions_mut().insert("list"); + self.request::>(req).await } } #[cfg(test)] #[cfg(feature = "client")] mod test { - use super::{Client, ListParams, Namespace}; + use super::{Client, Cluster, ListParams, Namespace}; use kube_core::ResourceExt; #[tokio::test] @@ -225,22 +173,22 @@ mod test { let client = Client::try_default().await?; let lp = ListParams::default(); // cluster-scoped list - for ns in client.list::(&lp).await? { + for ns in client.list::(&lp, &Cluster).await? { // namespaced list - for p in client.list_namespaced::(&lp, &(&ns).try_into()?).await? { + for p in client.list::(&lp, &Namespace::try_from(&ns)?).await? { println!("Found pod {} in {}", p.name_any(), ns.name_any()); } } // across-namespace list - for j in client.list_all::(&lp).await? { + for j in client.list::(&lp, &Cluster).await? { println!("Found job {} in {}", j.name_any(), j.namespace().unwrap()); } // namespaced get - let default: Namespace = "default".try_into()?; - let svc = client.get_namespaced::("kubernetes", &default).await?; + let default: Namespace = "default".into(); + let svc = client.get::("kubernetes", &default).await?; assert_eq!(svc.name_unchecked(), "kubernetes"); // global get - let ca = client.get::("cluster-admin").await?; + let ca = client.get::("cluster-admin", &Cluster).await?; assert_eq!(ca.name_unchecked(), "cluster-admin"); Ok(()) From 5d34d77b5483d0516062fbee0b13aade6196db85 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Natalie=20Klestrup=20R=C3=B6ijezon?= Date: Thu, 29 Feb 2024 15:49:03 +0100 Subject: [PATCH 07/16] Support dynamic resource scopes --- kube-client/src/client/client_ext.rs | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/kube-client/src/client/client_ext.rs b/kube-client/src/client/client_ext.rs index 0d4595167..c8d97c28b 100644 --- a/kube-client/src/client/client_ext.rs +++ b/kube-client/src/client/client_ext.rs @@ -3,13 +3,21 @@ use kube_core::{ object::ObjectList, params::{GetParams, ListParams}, request::Request, - ClusterResourceScope, NamespaceResourceScope, Resource, + ClusterResourceScope, DynamicResourceScope, NamespaceResourceScope, Resource, }; use serde::{de::DeserializeOwned, Serialize}; use std::fmt::Debug; use k8s_openapi::api::core::v1::Namespace as k8sNs; +// Allow dynamic scopes to "impersonate" both cluster- and namespace scopes +trait IsClusterScoped {} +impl IsClusterScoped for ClusterResourceScope {} +impl IsClusterScoped for DynamicResourceScope {} +trait IsNamespaceScoped {} +impl IsNamespaceScoped for NamespaceResourceScope {} +impl IsNamespaceScoped for DynamicResourceScope {} + pub trait ListScope { fn url_path(&self) -> String; } @@ -34,8 +42,9 @@ where // Only cluster-scoped objects can be named globally impl ObjectScope for Cluster where - K: Resource, + K: Resource, K::DynamicType: Default, + K::Scope: IsClusterScoped, { fn url_path(&self) -> String { K::url_path(&K::DynamicType::default(), None) @@ -47,8 +56,9 @@ pub struct Namespace(String); // Only namespaced objects can be accessed via namespace impl ListScope for Namespace where - K: Resource, + K: Resource, K::DynamicType: Default, + K::Scope: IsNamespaceScoped, { fn url_path(&self) -> String { K::url_path(&K::DynamicType::default(), Some(&self.0)) @@ -57,8 +67,9 @@ where impl ObjectScope for Namespace where - K: Resource, + K: Resource, K::DynamicType: Default, + K::Scope: IsNamespaceScoped, { fn url_path(&self) -> String { K::url_path(&K::DynamicType::default(), Some(&self.0)) From 9b46d4937c72a65174517464e6f3a4fb8a078705 Mon Sep 17 00:00:00 2001 From: clux Date: Wed, 13 Mar 2024 21:02:09 +0000 Subject: [PATCH 08/16] rename to more ergonomic names and update docs Signed-off-by: clux --- kube-client/src/client/client_ext.rs | 77 ++++++++++++++++++---------- kube-client/src/client/mod.rs | 17 ++++-- 2 files changed, 61 insertions(+), 33 deletions(-) diff --git a/kube-client/src/client/client_ext.rs b/kube-client/src/client/client_ext.rs index c8d97c28b..4f7304947 100644 --- a/kube-client/src/client/client_ext.rs +++ b/kube-client/src/client/client_ext.rs @@ -1,4 +1,5 @@ use crate::{Client, Error, Result}; +use k8s_openapi::api::core::v1::Namespace as k8sNs; use kube_core::{ object::ObjectList, params::{GetParams, ListParams}, @@ -8,28 +9,39 @@ use kube_core::{ use serde::{de::DeserializeOwned, Serialize}; use std::fmt::Debug; -use k8s_openapi::api::core::v1::Namespace as k8sNs; - -// Allow dynamic scopes to "impersonate" both cluster- and namespace scopes -trait IsClusterScoped {} -impl IsClusterScoped for ClusterResourceScope {} -impl IsClusterScoped for DynamicResourceScope {} -trait IsNamespaceScoped {} -impl IsNamespaceScoped for NamespaceResourceScope {} -impl IsNamespaceScoped for DynamicResourceScope {} - -pub trait ListScope { +/// A marker trait to indicate cluster-wide operations are available +trait ClusterScope {} +/// A marker trait to indicate namespace-scoped operations are available +trait NamespaceScope {} + +// k8s_openapi scopes get implementations for free +impl ClusterScope for ClusterResourceScope {} +impl NamespaceScope for NamespaceResourceScope {} +// our DynamicResourceScope can masquerade as either +impl NamespaceScope for DynamicResourceScope {} +impl ClusterScope for DynamicResourceScope {} + +/// How to get the url for a collection +/// +/// Pick one of `kube::client::Cluster` or `kube::client::Namespace`. +pub trait CollectionUrl { fn url_path(&self) -> String; } -pub trait ObjectScope { +/// How to get the url for an object +/// +/// Pick one of `kube::client::Cluster` or `kube::client::Namespace`. +pub trait ObjectUrl { fn url_path(&self) -> String; } +/// Marker type for cluster level queries pub struct Cluster; +/// Namespace newtype for namespace level queries +pub struct Namespace(String); // All objects can be listed cluster-wide -impl ListScope for Cluster +impl CollectionUrl for Cluster where K: Resource, K::DynamicType: Default, @@ -40,36 +52,34 @@ where } // Only cluster-scoped objects can be named globally -impl ObjectScope for Cluster +impl ObjectUrl for Cluster where K: Resource, K::DynamicType: Default, - K::Scope: IsClusterScoped, + K::Scope: ClusterScope, { fn url_path(&self) -> String { K::url_path(&K::DynamicType::default(), None) } } -pub struct Namespace(String); - // Only namespaced objects can be accessed via namespace -impl ListScope for Namespace +impl CollectionUrl for Namespace where K: Resource, K::DynamicType: Default, - K::Scope: IsNamespaceScoped, + K::Scope: NamespaceScope, { fn url_path(&self) -> String { K::url_path(&K::DynamicType::default(), Some(&self.0)) } } -impl ObjectScope for Namespace +impl ObjectUrl for Namespace where K: Resource, K::DynamicType: Default, - K::Scope: IsNamespaceScoped, + K::Scope: NamespaceScope, { fn url_path(&self) -> String { K::url_path(&K::DynamicType::default(), Some(&self.0)) @@ -109,22 +119,31 @@ pub enum NamespaceError { } /// Client extensions to allow typed api calls without [`Api`] +/// +/// These methods allow users to query across a wide-array of resources without needing +/// to explicitly create an `Api` for each one of them. +/// +/// The tradeoff is that you need to explicitly: +/// - specify the level you are querying at via [`Cluster`] or [`Namespace`] as args +/// - specify the resource type you are using for serialization (e.g. a top level k8s-openapi type) impl Client { /// Get a resource /// /// ```no_run /// # use k8s_openapi::api::rbac::v1::ClusterRole; + /// # use k8s_openapi::api::core::v1::Service; + /// # use kube::client::{Cluster, Namespace}; /// # use kube::{ResourceExt, api::GetParams}; /// # async fn wrapper() -> Result<(), Box> { /// # let client: kube::Client = todo!(); - /// let crole = client.get::("cluster-admin", &Cluster).await?; - /// assert_eq!(crole.name_unchecked(), "cluster-admin"); + /// let cr = client.get::("cluster-admin", &Cluster).await?; + /// assert_eq!(cr.name_unchecked(), "cluster-admin"); /// let svc = client.get::("kubernetes", &Namespace::from("default")).await?; /// assert_eq!(svc.name_unchecked(), "kubernetes"); /// # Ok(()) /// # } /// ``` - pub async fn get(&self, name: &str, scope: &impl ObjectScope) -> Result + pub async fn get(&self, name: &str, scope: &impl ObjectUrl) -> Result where K: Resource + Serialize + DeserializeOwned + Clone + Debug, ::DynamicType: Default, @@ -139,13 +158,15 @@ impl Client { /// List a resource /// /// ```no_run - /// # use k8s_openapi::api::rbac::v1::ClusterRole; + /// # use k8s_openapi::api::core::v1::Pod; + /// # use k8s_openapi::api::core::v1::Service; + /// # use kube::client::{Cluster, Namespace}; /// # use kube::{ResourceExt, api::ListParams}; /// # async fn wrapper() -> Result<(), Box> { /// # let client: kube::Client = todo!(); /// let lp = ListParams::default(); - /// for svc in client.list::(&lp).await? { - /// println!("Found clusterrole {}", svc.name_any()); + /// for pod in client.list::(&lp, &Cluster).await? { + /// println!("Found pod {} in {}", pod.name_any(), pod.namespace().unwrap()); /// } /// for svc in client.list::(&lp, &Namespace::from("default")).await? { /// println!("Found service {}", svc.name_any()); @@ -153,7 +174,7 @@ impl Client { /// # Ok(()) /// # } /// ``` - pub async fn list(&self, lp: &ListParams, scope: &impl ListScope) -> Result> + pub async fn list(&self, lp: &ListParams, scope: &impl CollectionUrl) -> Result> where K: Resource + Serialize + DeserializeOwned + Clone + Debug, ::DynamicType: Default, diff --git a/kube-client/src/client/mod.rs b/kube-client/src/client/mod.rs index ee6ef0739..8c85c1f66 100644 --- a/kube-client/src/client/mod.rs +++ b/kube-client/src/client/mod.rs @@ -31,17 +31,23 @@ mod body; mod builder; // Add `into_stream()` to `http::Body` use body::BodyStreamExt; -#[cfg(feature = "unstable-client")] mod client_ext; +#[cfg(feature = "unstable-client")] +mod client_ext; +#[cfg(feature = "unstable-client")] +pub use client_ext::{Cluster, Namespace}; mod config_ext; pub use auth::Error as AuthError; pub use config_ext::ConfigExt; pub mod middleware; -#[cfg(any(feature = "rustls-tls", feature = "openssl-tls"))] mod tls; +#[cfg(any(feature = "rustls-tls", feature = "openssl-tls"))] +mod tls; #[cfg(feature = "openssl-tls")] pub use tls::openssl_tls::Error as OpensslTlsError; -#[cfg(feature = "rustls-tls")] pub use tls::rustls_tls::Error as RustlsTlsError; -#[cfg(feature = "ws")] mod upgrade; +#[cfg(feature = "rustls-tls")] +pub use tls::rustls_tls::Error as RustlsTlsError; +#[cfg(feature = "ws")] +mod upgrade; #[cfg(feature = "oauth")] #[cfg_attr(docsrs, doc(cfg(feature = "oauth")))] @@ -51,7 +57,8 @@ pub use auth::OAuthError; #[cfg_attr(docsrs, doc(cfg(feature = "oidc")))] pub use auth::oidc_errors; -#[cfg(feature = "ws")] pub use upgrade::UpgradeConnectionError; +#[cfg(feature = "ws")] +pub use upgrade::UpgradeConnectionError; pub use builder::{ClientBuilder, DynBody}; From c26ea13b605d761d929fb0cdc2a446c319db0b93 Mon Sep 17 00:00:00 2001 From: clux Date: Wed, 13 Mar 2024 21:22:09 +0000 Subject: [PATCH 09/16] tests + an import idea Signed-off-by: clux --- examples/node_watcher.rs | 4 ++-- kube-client/src/client/client_ext.rs | 5 +++++ kube-client/src/client/mod.rs | 2 +- kube-client/src/discovery/apigroup.rs | 1 + 4 files changed, 9 insertions(+), 3 deletions(-) diff --git a/examples/node_watcher.rs b/examples/node_watcher.rs index 42054cc64..a27756a26 100644 --- a/examples/node_watcher.rs +++ b/examples/node_watcher.rs @@ -2,8 +2,8 @@ use futures::{pin_mut, TryStreamExt}; use k8s_openapi::api::core::v1::{Event, Node}; use kube::{ api::{Api, ListParams, ResourceExt}, + client::{scope, Client}, runtime::{watcher, WatchStreamExt}, - Client, }; use tracing::*; @@ -51,7 +51,7 @@ async fn check_for_node_failures(client: &Client, o: Node) -> anyhow::Result<()> // Find events related to this node let opts = ListParams::default().fields(&format!("involvedObject.kind=Node,involvedObject.name={name}")); - let evlist = client.list_all::(&opts).await?; + let evlist = client.list::(&opts, &scope::Cluster).await?; for e in evlist { warn!("Node event: {:?}", serde_json::to_string_pretty(&e)?); } diff --git a/kube-client/src/client/client_ext.rs b/kube-client/src/client/client_ext.rs index 4f7304947..03455128c 100644 --- a/kube-client/src/client/client_ext.rs +++ b/kube-client/src/client/client_ext.rs @@ -40,6 +40,11 @@ pub struct Cluster; /// Namespace newtype for namespace level queries pub struct Namespace(String); +/// Module for scope for ease of importing +pub mod scope { + pub use super::{Cluster, Namespace}; +} + // All objects can be listed cluster-wide impl CollectionUrl for Cluster where diff --git a/kube-client/src/client/mod.rs b/kube-client/src/client/mod.rs index 8c85c1f66..29f250c93 100644 --- a/kube-client/src/client/mod.rs +++ b/kube-client/src/client/mod.rs @@ -34,7 +34,7 @@ use body::BodyStreamExt; #[cfg(feature = "unstable-client")] mod client_ext; #[cfg(feature = "unstable-client")] -pub use client_ext::{Cluster, Namespace}; +pub use client_ext::scope; mod config_ext; pub use auth::Error as AuthError; pub use config_ext::ConfigExt; diff --git a/kube-client/src/discovery/apigroup.rs b/kube-client/src/discovery/apigroup.rs index 42f2f1740..e13329e7e 100644 --- a/kube-client/src/discovery/apigroup.rs +++ b/kube-client/src/discovery/apigroup.rs @@ -1,6 +1,7 @@ use super::parse::{self, GroupVersionData}; use crate::{error::DiscoveryError, Client, Error, Result}; use k8s_openapi::apimachinery::pkg::apis::meta::v1::{APIGroup, APIVersions}; +#[allow(unused_imports)] pub use kube_core::discovery::{verbs, ApiCapabilities, ApiResource, Scope}; use kube_core::{ gvk::{GroupVersion, GroupVersionKind, ParseGroupVersionError}, From 761b32496ece7fabab868c1e2b2ccf95a0896eb0 Mon Sep 17 00:00:00 2001 From: clux Date: Wed, 13 Mar 2024 21:43:46 +0000 Subject: [PATCH 10/16] add some docs to distinguish the ext method block from the ctor block Signed-off-by: clux --- kube-client/Cargo.toml | 2 +- kube-client/src/client/client_ext.rs | 36 ++++++++++++++++++++++------ kube-client/src/client/mod.rs | 17 ++++++++++++- 3 files changed, 46 insertions(+), 9 deletions(-) diff --git a/kube-client/Cargo.toml b/kube-client/Cargo.toml index 922b4948a..aa757bb6f 100644 --- a/kube-client/Cargo.toml +++ b/kube-client/Cargo.toml @@ -34,7 +34,7 @@ unstable-client = [] __non_core = ["tracing", "serde_yaml", "base64"] [package.metadata.docs.rs] -features = ["client", "rustls-tls", "openssl-tls", "ws", "oauth", "oidc", "jsonpatch", "admission", "k8s-openapi/latest", "socks5"] +features = ["client", "rustls-tls", "openssl-tls", "ws", "oauth", "oidc", "jsonpatch", "admission", "k8s-openapi/latest", "socks5", "unstable-client"] # Define the configuration attribute `docsrs`. Used to enable `doc_cfg` feature. rustdoc-args = ["--cfg", "docsrs"] diff --git a/kube-client/src/client/client_ext.rs b/kube-client/src/client/client_ext.rs index 03455128c..ba1281dad 100644 --- a/kube-client/src/client/client_ext.rs +++ b/kube-client/src/client/client_ext.rs @@ -123,16 +123,38 @@ pub enum NamespaceError { MissingName, } -/// Client extensions to allow typed api calls without [`Api`] +/// Generic client extensions for the `unstable-client` feature /// /// These methods allow users to query across a wide-array of resources without needing -/// to explicitly create an `Api` for each one of them. +/// to explicitly create an [`Api`](crate::Api) for each one of them. /// -/// The tradeoff is that you need to explicitly: -/// - specify the level you are querying at via [`Cluster`] or [`Namespace`] as args -/// - specify the resource type you are using for serialization (e.g. a top level k8s-openapi type) +/// ## Usage +/// 1. Create a [`Client`] +/// 2. Specify the level you are querying at via [`Cluster`] or [`Namespace`] as args +/// 3. Specify the resource type you are using for serialization (e.g. a top level k8s-openapi type) +/// +/// ## Example +/// +/// ```no_run +/// # use k8s_openapi::api::core::v1::Pod; +/// # use k8s_openapi::api::core::v1::Service; +/// # use kube::client::{Cluster, Namespace}; +/// # use kube::{ResourceExt, api::ListParams}; +/// # async fn wrapper() -> Result<(), Box> { +/// # let client: kube::Client = todo!(); +/// let lp = ListParams::default(); +/// # List at cluster level: +/// for pod in client.list::(&lp, &Cluster).await? { +/// println!("Found pod {} in {}", pod.name_any(), pod.namespace().unwrap()); +/// } +/// # Namespaced get: +/// let svc = client.get::("kubernetes", &Namespace::from("default")).await?; +/// assert_eq!(svc.name_unchecked(), "kubernetes"); +/// # Ok(()) +/// # } +/// ``` impl Client { - /// Get a resource + /// Get a single instance of a `Resource` implementing type `K` at the specified scope. /// /// ```no_run /// # use k8s_openapi::api::rbac::v1::ClusterRole; @@ -160,7 +182,7 @@ impl Client { self.request::(req).await } - /// List a resource + /// List instances of a `Resource` implementing type `K` at the specified scope. /// /// ```no_run /// # use k8s_openapi::api::core::v1::Pod; diff --git a/kube-client/src/client/mod.rs b/kube-client/src/client/mod.rs index 29f250c93..0db360f54 100644 --- a/kube-client/src/client/mod.rs +++ b/kube-client/src/client/mod.rs @@ -77,6 +77,11 @@ pub struct Client { default_ns: String, } +/// Constructors and low-level api interfaces. +/// +/// Most users only need [`Client::try_default`] or [`Client::new`] from this block. +/// +/// The many various lower level interfaces here are for more advanced use-cases with specific requirements. impl Client { /// Create a [`Client`] using a custom `Service` stack. /// @@ -131,6 +136,14 @@ impl Client { /// /// Will fail if neither configuration could be loaded. /// + /// ```rust + /// # async fn doc() -> Result<(), Box> { + /// # use kube::Client; + /// let client = Client::try_default().await?; + /// # Ok(()) + /// # } + /// ``` + /// /// If you already have a [`Config`] then use [`Client::try_from`](Self::try_from) /// instead. pub async fn try_default() -> Result { @@ -468,7 +481,9 @@ fn handle_api_errors(text: &str, s: StatusCode) -> Result<()> { impl TryFrom for Client { type Error = Error; - /// Builds a default [`Client`] from a [`Config`], see [`ClientBuilder`] if more customization is required + /// Builds a default [`Client`] from a [`Config`]. + /// + /// See [`ClientBuilder`] or [`Client::new`] if more customization is required fn try_from(config: Config) -> Result { Ok(ClientBuilder::try_from(config)?.build()) } From dbc8d8d1ab952ce814b6297fc9d637646ec970df Mon Sep 17 00:00:00 2001 From: clux Date: Wed, 13 Mar 2024 21:54:01 +0000 Subject: [PATCH 11/16] fix doc tests Signed-off-by: clux --- kube-client/src/client/client_ext.rs | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/kube-client/src/client/client_ext.rs b/kube-client/src/client/client_ext.rs index ba1281dad..1b6b85d3e 100644 --- a/kube-client/src/client/client_ext.rs +++ b/kube-client/src/client/client_ext.rs @@ -138,16 +138,16 @@ pub enum NamespaceError { /// ```no_run /// # use k8s_openapi::api::core::v1::Pod; /// # use k8s_openapi::api::core::v1::Service; -/// # use kube::client::{Cluster, Namespace}; +/// # use kube::client::scope::{Cluster, Namespace}; /// # use kube::{ResourceExt, api::ListParams}; /// # async fn wrapper() -> Result<(), Box> { /// # let client: kube::Client = todo!(); /// let lp = ListParams::default(); -/// # List at cluster level: +/// // List at Cluster level: /// for pod in client.list::(&lp, &Cluster).await? { /// println!("Found pod {} in {}", pod.name_any(), pod.namespace().unwrap()); /// } -/// # Namespaced get: +/// // Namespaced Get: /// let svc = client.get::("kubernetes", &Namespace::from("default")).await?; /// assert_eq!(svc.name_unchecked(), "kubernetes"); /// # Ok(()) @@ -159,7 +159,7 @@ impl Client { /// ```no_run /// # use k8s_openapi::api::rbac::v1::ClusterRole; /// # use k8s_openapi::api::core::v1::Service; - /// # use kube::client::{Cluster, Namespace}; + /// # use kube::client::scope::{Cluster, Namespace}; /// # use kube::{ResourceExt, api::GetParams}; /// # async fn wrapper() -> Result<(), Box> { /// # let client: kube::Client = todo!(); @@ -187,7 +187,7 @@ impl Client { /// ```no_run /// # use k8s_openapi::api::core::v1::Pod; /// # use k8s_openapi::api::core::v1::Service; - /// # use kube::client::{Cluster, Namespace}; + /// # use kube::client::scope::{Cluster, Namespace}; /// # use kube::{ResourceExt, api::ListParams}; /// # async fn wrapper() -> Result<(), Box> { /// # let client: kube::Client = todo!(); @@ -217,7 +217,8 @@ impl Client { #[cfg(test)] #[cfg(feature = "client")] mod test { - use super::{Client, Cluster, ListParams, Namespace}; + use super::scope::{Cluster, Namespace}; + use super::{Client, ListParams}; use kube_core::ResourceExt; #[tokio::test] From 106c22f17ee935f48490539ba0ef119e4f335914 Mon Sep 17 00:00:00 2001 From: clux Date: Wed, 13 Mar 2024 21:56:15 +0000 Subject: [PATCH 12/16] ugh a special case in find broke fmt for me Signed-off-by: clux --- kube-client/src/client/client_ext.rs | 6 ++++-- kube-client/src/client/mod.rs | 18 ++++++------------ 2 files changed, 10 insertions(+), 14 deletions(-) diff --git a/kube-client/src/client/client_ext.rs b/kube-client/src/client/client_ext.rs index 1b6b85d3e..096db82fd 100644 --- a/kube-client/src/client/client_ext.rs +++ b/kube-client/src/client/client_ext.rs @@ -217,8 +217,10 @@ impl Client { #[cfg(test)] #[cfg(feature = "client")] mod test { - use super::scope::{Cluster, Namespace}; - use super::{Client, ListParams}; + use super::{ + scope::{Cluster, Namespace}, + Client, ListParams, + }; use kube_core::ResourceExt; #[tokio::test] diff --git a/kube-client/src/client/mod.rs b/kube-client/src/client/mod.rs index 0db360f54..442c9e2cb 100644 --- a/kube-client/src/client/mod.rs +++ b/kube-client/src/client/mod.rs @@ -31,23 +31,18 @@ mod body; mod builder; // Add `into_stream()` to `http::Body` use body::BodyStreamExt; -#[cfg(feature = "unstable-client")] -mod client_ext; -#[cfg(feature = "unstable-client")] -pub use client_ext::scope; +#[cfg(feature = "unstable-client")] mod client_ext; +#[cfg(feature = "unstable-client")] pub use client_ext::scope; mod config_ext; pub use auth::Error as AuthError; pub use config_ext::ConfigExt; pub mod middleware; -#[cfg(any(feature = "rustls-tls", feature = "openssl-tls"))] -mod tls; +#[cfg(any(feature = "rustls-tls", feature = "openssl-tls"))] mod tls; #[cfg(feature = "openssl-tls")] pub use tls::openssl_tls::Error as OpensslTlsError; -#[cfg(feature = "rustls-tls")] -pub use tls::rustls_tls::Error as RustlsTlsError; -#[cfg(feature = "ws")] -mod upgrade; +#[cfg(feature = "rustls-tls")] pub use tls::rustls_tls::Error as RustlsTlsError; +#[cfg(feature = "ws")] mod upgrade; #[cfg(feature = "oauth")] #[cfg_attr(docsrs, doc(cfg(feature = "oauth")))] @@ -57,8 +52,7 @@ pub use auth::OAuthError; #[cfg_attr(docsrs, doc(cfg(feature = "oidc")))] pub use auth::oidc_errors; -#[cfg(feature = "ws")] -pub use upgrade::UpgradeConnectionError; +#[cfg(feature = "ws")] pub use upgrade::UpgradeConnectionError; pub use builder::{ClientBuilder, DynBody}; From c3d275593b4299fa5ccb2335494b252e6efd4174 Mon Sep 17 00:00:00 2001 From: clux Date: Mon, 18 Mar 2024 12:26:39 +0000 Subject: [PATCH 13/16] properly fix unused import warning in discovery was originally afraid to remove this since it's a pub re-export but it's only pub for this module, the root `mod.rs` does not re-export so have moved the only import to where it is needed Signed-off-by: clux --- kube-client/src/discovery/apigroup.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/kube-client/src/discovery/apigroup.rs b/kube-client/src/discovery/apigroup.rs index e13329e7e..d7a7557a3 100644 --- a/kube-client/src/discovery/apigroup.rs +++ b/kube-client/src/discovery/apigroup.rs @@ -1,8 +1,7 @@ use super::parse::{self, GroupVersionData}; use crate::{error::DiscoveryError, Client, Error, Result}; use k8s_openapi::apimachinery::pkg::apis::meta::v1::{APIGroup, APIVersions}; -#[allow(unused_imports)] -pub use kube_core::discovery::{verbs, ApiCapabilities, ApiResource, Scope}; +pub use kube_core::discovery::{ApiCapabilities, ApiResource}; use kube_core::{ gvk::{GroupVersion, GroupVersionKind, ParseGroupVersionError}, Version, @@ -328,6 +327,7 @@ impl ApiGroup { #[cfg(test)] mod tests { use super::*; + use kube_core::discovery::Scope; #[test] fn test_resources_by_stability() { From 44a740bce2a857bc8c3be7bfec8d25021ceff84a Mon Sep 17 00:00:00 2001 From: clux Date: Mon, 18 Mar 2024 13:30:36 +0000 Subject: [PATCH 14/16] no need to confuse features around client ext tests these tests should run if the parent module is included Signed-off-by: clux --- kube-client/src/client/client_ext.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/kube-client/src/client/client_ext.rs b/kube-client/src/client/client_ext.rs index 096db82fd..7987fe81a 100644 --- a/kube-client/src/client/client_ext.rs +++ b/kube-client/src/client/client_ext.rs @@ -215,7 +215,6 @@ impl Client { } #[cfg(test)] -#[cfg(feature = "client")] mod test { use super::{ scope::{Cluster, Namespace}, From b24e88a60b3cb0dbd26a793973c657d3cf5ae6ab Mon Sep 17 00:00:00 2001 From: clux Date: Mon, 18 Mar 2024 14:27:52 +0000 Subject: [PATCH 15/16] better docs for client-ext related stuff - docsrs feature limiters, for best effort help - plus a couple of broken links Signed-off-by: clux --- kube-client/src/client/client_ext.rs | 12 ++++++++---- kube-client/src/client/mod.rs | 8 ++++++-- kube-client/src/error.rs | 6 +++--- kube-client/src/lib.rs | 2 +- 4 files changed, 18 insertions(+), 10 deletions(-) diff --git a/kube-client/src/client/client_ext.rs b/kube-client/src/client/client_ext.rs index 7987fe81a..7f9fe60df 100644 --- a/kube-client/src/client/client_ext.rs +++ b/kube-client/src/client/client_ext.rs @@ -38,9 +38,12 @@ pub trait ObjectUrl { /// Marker type for cluster level queries pub struct Cluster; /// Namespace newtype for namespace level queries +/// +/// You can create this directly, or convert `From` a `String` / `&str`, or `TryFrom` an `k8s_openapi::api::core::v1::Namespace` pub struct Namespace(String); -/// Module for scope for ease of importing +/// Scopes for `unstable-client` [`Client#impl-Client`] extension methods +#[cfg_attr(docsrs, doc(cfg(any(feature = "unstable-client"))))] pub mod scope { pub use super::{Cluster, Namespace}; } @@ -130,7 +133,7 @@ pub enum NamespaceError { /// /// ## Usage /// 1. Create a [`Client`] -/// 2. Specify the level you are querying at via [`Cluster`] or [`Namespace`] as args +/// 2. Specify the [`scope`] you are querying at via [`Cluster`] or [`Namespace`] as args /// 3. Specify the resource type you are using for serialization (e.g. a top level k8s-openapi type) /// /// ## Example @@ -143,16 +146,17 @@ pub enum NamespaceError { /// # async fn wrapper() -> Result<(), Box> { /// # let client: kube::Client = todo!(); /// let lp = ListParams::default(); -/// // List at Cluster level: +/// // List at Cluster level for Pod resource: /// for pod in client.list::(&lp, &Cluster).await? { /// println!("Found pod {} in {}", pod.name_any(), pod.namespace().unwrap()); /// } -/// // Namespaced Get: +/// // Namespaced Get for Service resource: /// let svc = client.get::("kubernetes", &Namespace::from("default")).await?; /// assert_eq!(svc.name_unchecked(), "kubernetes"); /// # Ok(()) /// # } /// ``` +#[cfg_attr(docsrs, doc(cfg(feature = "unstable-client")))] impl Client { /// Get a single instance of a `Resource` implementing type `K` at the specified scope. /// diff --git a/kube-client/src/client/mod.rs b/kube-client/src/client/mod.rs index 442c9e2cb..6485d5896 100644 --- a/kube-client/src/client/mod.rs +++ b/kube-client/src/client/mod.rs @@ -31,8 +31,12 @@ mod body; mod builder; // Add `into_stream()` to `http::Body` use body::BodyStreamExt; -#[cfg(feature = "unstable-client")] mod client_ext; -#[cfg(feature = "unstable-client")] pub use client_ext::scope; +#[cfg_attr(docsrs, doc(cfg(feature = "unstable-client")))] +#[cfg(feature = "unstable-client")] +mod client_ext; +#[cfg_attr(docsrs, doc(cfg(feature = "unstable-client")))] +#[cfg(feature = "unstable-client")] +pub use client_ext::scope; mod config_ext; pub use auth::Error as AuthError; pub use config_ext::ConfigExt; diff --git a/kube-client/src/error.rs b/kube-client/src/error.rs index bc15beb2c..09aff9164 100644 --- a/kube-client/src/error.rs +++ b/kube-client/src/error.rs @@ -1,9 +1,9 @@ -//! Error handling in [`kube`][crate] +//! Error handling and error types use thiserror::Error; pub use kube_core::ErrorResponse; -/// Possible errors when working with [`kube`][crate] +/// Possible errors from the [`Client`](crate::Client) #[cfg_attr(docsrs, doc(cfg(any(feature = "config", feature = "client"))))] #[derive(Error, Debug)] pub enum Error { @@ -89,7 +89,7 @@ pub enum Error { } #[derive(Error, Debug)] -/// Possible errors when using API discovery +/// Possible errors when using API [discovery](crate::discovery) pub enum DiscoveryError { /// Invalid GroupVersion #[error("Invalid GroupVersion: {0}")] diff --git a/kube-client/src/lib.rs b/kube-client/src/lib.rs index d662ff4fa..427b4e25f 100644 --- a/kube-client/src/lib.rs +++ b/kube-client/src/lib.rs @@ -59,7 +59,7 @@ //! - [`Client`](crate::client) for the extensible Kubernetes client //! - [`Config`](crate::config) for the Kubernetes config abstraction //! - [`Api`](crate::Api) for the generic api methods available on Kubernetes resources -//! - [k8s-openapi](https://docs.rs/k8s-openapi/*/k8s_openapi/) for how to create typed kubernetes objects directly +//! - [k8s-openapi](https://docs.rs/k8s-openapi) for how to create typed kubernetes objects directly #![cfg_attr(docsrs, feature(doc_cfg))] #![deny(missing_docs)] #![forbid(unsafe_code)] From 87ec0447cbc1828820489687aaeb339852c02589 Mon Sep 17 00:00:00 2001 From: clux Date: Mon, 18 Mar 2024 15:56:24 +0000 Subject: [PATCH 16/16] no need for inner docsrs doccfg attrs Signed-off-by: clux --- kube-client/src/client/client_ext.rs | 2 -- kube-client/src/config/mod.rs | 2 +- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/kube-client/src/client/client_ext.rs b/kube-client/src/client/client_ext.rs index 7f9fe60df..48704ea07 100644 --- a/kube-client/src/client/client_ext.rs +++ b/kube-client/src/client/client_ext.rs @@ -43,7 +43,6 @@ pub struct Cluster; pub struct Namespace(String); /// Scopes for `unstable-client` [`Client#impl-Client`] extension methods -#[cfg_attr(docsrs, doc(cfg(any(feature = "unstable-client"))))] pub mod scope { pub use super::{Cluster, Namespace}; } @@ -156,7 +155,6 @@ pub enum NamespaceError { /// # Ok(()) /// # } /// ``` -#[cfg_attr(docsrs, doc(cfg(feature = "unstable-client")))] impl Client { /// Get a single instance of a `Resource` implementing type `K` at the specified scope. /// diff --git a/kube-client/src/config/mod.rs b/kube-client/src/config/mod.rs index 3d9e2be77..a60935960 100644 --- a/kube-client/src/config/mod.rs +++ b/kube-client/src/config/mod.rs @@ -122,7 +122,7 @@ pub enum LoadDataError { /// Prefer [`Config::infer`] unless you have particular issues, and avoid manually managing /// the data in this struct unless you have particular needs. It exists to be consumed by the [`Client`][crate::Client]. /// -/// If you are looking to parse the kubeconfig found in a user's home directory see [`Kubeconfig`](crate::config::Kubeconfig). +/// If you are looking to parse the kubeconfig found in a user's home directory see [`Kubeconfig`]. #[cfg_attr(docsrs, doc(cfg(feature = "config")))] #[derive(Debug, Clone)] pub struct Config {