-
-
Notifications
You must be signed in to change notification settings - Fork 326
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge branch 'main' into direct-node
- Loading branch information
Showing
14 changed files
with
306 additions
and
23 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,259 @@ | ||
use crate::{Client, Error, Result}; | ||
use k8s_openapi::api::core::v1::Namespace as k8sNs; | ||
use kube_core::{ | ||
object::ObjectList, | ||
params::{GetParams, ListParams}, | ||
request::Request, | ||
ClusterResourceScope, DynamicResourceScope, NamespaceResourceScope, Resource, | ||
}; | ||
use serde::{de::DeserializeOwned, Serialize}; | ||
use std::fmt::Debug; | ||
|
||
/// 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<K> { | ||
fn url_path(&self) -> String; | ||
} | ||
|
||
/// How to get the url for an object | ||
/// | ||
/// Pick one of `kube::client::Cluster` or `kube::client::Namespace`. | ||
pub trait ObjectUrl<K> { | ||
fn url_path(&self) -> String; | ||
} | ||
|
||
/// 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); | ||
|
||
/// Scopes for `unstable-client` [`Client#impl-Client`] extension methods | ||
pub mod scope { | ||
pub use super::{Cluster, Namespace}; | ||
} | ||
|
||
// All objects can be listed cluster-wide | ||
impl<K> CollectionUrl<K> 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<K> ObjectUrl<K> for Cluster | ||
where | ||
K: Resource, | ||
K::DynamicType: Default, | ||
K::Scope: ClusterScope, | ||
{ | ||
fn url_path(&self) -> String { | ||
K::url_path(&K::DynamicType::default(), None) | ||
} | ||
} | ||
|
||
// Only namespaced objects can be accessed via namespace | ||
impl<K> CollectionUrl<K> for Namespace | ||
where | ||
K: Resource, | ||
K::DynamicType: Default, | ||
K::Scope: NamespaceScope, | ||
{ | ||
fn url_path(&self) -> String { | ||
K::url_path(&K::DynamicType::default(), Some(&self.0)) | ||
} | ||
} | ||
|
||
impl<K> ObjectUrl<K> for Namespace | ||
where | ||
K: Resource, | ||
K::DynamicType: Default, | ||
K::Scope: NamespaceScope, | ||
{ | ||
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; | ||
|
||
fn try_from(ns: &k8sNs) -> Result<Namespace, Self::Error> { | ||
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<String> 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, | ||
} | ||
|
||
/// 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`](crate::Api) for each one of them. | ||
/// | ||
/// ## Usage | ||
/// 1. Create a [`Client`] | ||
/// 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 | ||
/// | ||
/// ```no_run | ||
/// # use k8s_openapi::api::core::v1::Pod; | ||
/// # use k8s_openapi::api::core::v1::Service; | ||
/// # use kube::client::scope::{Cluster, Namespace}; | ||
/// # use kube::{ResourceExt, api::ListParams}; | ||
/// # async fn wrapper() -> Result<(), Box<dyn std::error::Error>> { | ||
/// # let client: kube::Client = todo!(); | ||
/// let lp = ListParams::default(); | ||
/// // List at Cluster level for Pod resource: | ||
/// for pod in client.list::<Pod>(&lp, &Cluster).await? { | ||
/// println!("Found pod {} in {}", pod.name_any(), pod.namespace().unwrap()); | ||
/// } | ||
/// // Namespaced Get for Service resource: | ||
/// let svc = client.get::<Service>("kubernetes", &Namespace::from("default")).await?; | ||
/// assert_eq!(svc.name_unchecked(), "kubernetes"); | ||
/// # Ok(()) | ||
/// # } | ||
/// ``` | ||
impl Client { | ||
/// Get a single instance of a `Resource` implementing type `K` at the specified scope. | ||
/// | ||
/// ```no_run | ||
/// # use k8s_openapi::api::rbac::v1::ClusterRole; | ||
/// # use k8s_openapi::api::core::v1::Service; | ||
/// # use kube::client::scope::{Cluster, Namespace}; | ||
/// # use kube::{ResourceExt, api::GetParams}; | ||
/// # async fn wrapper() -> Result<(), Box<dyn std::error::Error>> { | ||
/// # let client: kube::Client = todo!(); | ||
/// let cr = client.get::<ClusterRole>("cluster-admin", &Cluster).await?; | ||
/// assert_eq!(cr.name_unchecked(), "cluster-admin"); | ||
/// let svc = client.get::<Service>("kubernetes", &Namespace::from("default")).await?; | ||
/// assert_eq!(svc.name_unchecked(), "kubernetes"); | ||
/// # Ok(()) | ||
/// # } | ||
/// ``` | ||
pub async fn get<K>(&self, name: &str, scope: &impl ObjectUrl<K>) -> Result<K> | ||
where | ||
K: Resource + Serialize + DeserializeOwned + Clone + Debug, | ||
<K as Resource>::DynamicType: Default, | ||
{ | ||
let mut req = Request::new(scope.url_path()) | ||
.get(name, &GetParams::default()) | ||
.map_err(Error::BuildRequest)?; | ||
req.extensions_mut().insert("get"); | ||
self.request::<K>(req).await | ||
} | ||
|
||
/// List instances of a `Resource` implementing type `K` at the specified scope. | ||
/// | ||
/// ```no_run | ||
/// # use k8s_openapi::api::core::v1::Pod; | ||
/// # use k8s_openapi::api::core::v1::Service; | ||
/// # use kube::client::scope::{Cluster, Namespace}; | ||
/// # use kube::{ResourceExt, api::ListParams}; | ||
/// # async fn wrapper() -> Result<(), Box<dyn std::error::Error>> { | ||
/// # let client: kube::Client = todo!(); | ||
/// let lp = ListParams::default(); | ||
/// for pod in client.list::<Pod>(&lp, &Cluster).await? { | ||
/// println!("Found pod {} in {}", pod.name_any(), pod.namespace().unwrap()); | ||
/// } | ||
/// for svc in client.list::<Service>(&lp, &Namespace::from("default")).await? { | ||
/// println!("Found service {}", svc.name_any()); | ||
/// } | ||
/// # Ok(()) | ||
/// # } | ||
/// ``` | ||
pub async fn list<K>(&self, lp: &ListParams, scope: &impl CollectionUrl<K>) -> Result<ObjectList<K>> | ||
where | ||
K: Resource + Serialize + DeserializeOwned + Clone + Debug, | ||
<K as Resource>::DynamicType: Default, | ||
{ | ||
let mut req = Request::new(scope.url_path()) | ||
.list(lp) | ||
.map_err(Error::BuildRequest)?; | ||
req.extensions_mut().insert("list"); | ||
self.request::<ObjectList<K>>(req).await | ||
} | ||
} | ||
|
||
#[cfg(test)] | ||
mod test { | ||
use super::{ | ||
scope::{Cluster, Namespace}, | ||
Client, ListParams, | ||
}; | ||
use kube_core::ResourceExt; | ||
|
||
#[tokio::test] | ||
#[ignore = "needs cluster (will list/get namespaces, pods, jobs, svcs, clusterroles)"] | ||
async fn client_ext_list_get_pods_svcs() -> Result<(), Box<dyn std::error::Error>> { | ||
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::<k8sNs>(&lp, &Cluster).await? { | ||
// namespaced list | ||
for p in client.list::<Pod>(&lp, &Namespace::try_from(&ns)?).await? { | ||
println!("Found pod {} in {}", p.name_any(), ns.name_any()); | ||
} | ||
} | ||
// across-namespace list | ||
for j in client.list::<Job>(&lp, &Cluster).await? { | ||
println!("Found job {} in {}", j.name_any(), j.namespace().unwrap()); | ||
} | ||
// namespaced get | ||
let default: Namespace = "default".into(); | ||
let svc = client.get::<Service>("kubernetes", &default).await?; | ||
assert_eq!(svc.name_unchecked(), "kubernetes"); | ||
// global get | ||
let ca = client.get::<ClusterRole>("cluster-admin", &Cluster).await?; | ||
assert_eq!(ca.name_unchecked(), "cluster-admin"); | ||
|
||
Ok(()) | ||
} | ||
} |
Oops, something went wrong.