Skip to content

Commit

Permalink
Implement native object reference fetching
Browse files Browse the repository at this point in the history
Signed-off-by: Danil-Grigorev <[email protected]>
  • Loading branch information
Danil-Grigorev committed Jun 4, 2024
1 parent 6ce3978 commit a051537
Show file tree
Hide file tree
Showing 2 changed files with 175 additions and 4 deletions.
173 changes: 169 additions & 4 deletions kube-client/src/client/client_ext.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
use crate::{Client, Error, Result};
use k8s_openapi::api::core::v1::Namespace as k8sNs;
use k8s_openapi::api::core::v1::{Namespace as k8sNs, ObjectReference};
use kube_core::{
object::ObjectList,
params::{GetParams, ListParams},
Expand Down Expand Up @@ -42,9 +42,74 @@ pub struct Cluster;
/// You can create this directly, or convert `From` a `String` / `&str`, or `TryFrom` an `k8s_openapi::api::core::v1::Namespace`
pub struct Namespace(String);

/// How to get the url for an object
///
/// Pick one of `kube::client::Cluster` or `kube::client::Namespace`.
pub trait Reference<K>: ObjectUrl<K> {
fn name(&self) -> Option<&str>;
}

/// Ref<K> stores a resolvable namespaced or cluster scoped reference, which can be fetched by a client
pub enum Ref<K: Resource> {
/// Namespace stores a reference to a namespace scoped resource
Namespace(ResourceRef<K>),
/// Cluster stores a reference to a cluster scoped resource
Cluster(ResourceRef<K>),
}

/// ResourceRef provides a resolvable reference to an arbitrary object
#[derive(Clone, Debug, Default, PartialEq)]
pub struct ResourceRef<K: Resource> {
/// Type of the underlying object
pub dyntype: K::DynamicType,

/// Object reference
pub object_ref: ObjectReference,
}

impl<K> ResourceRef<K>
where
K: Resource,
K::DynamicType: Default,
{
/// Construct a new ResourceRef<K> from ObjectReference
pub fn new(object_ref: impl Into<Option<ObjectReference>>) -> Self {
Self {
object_ref: object_ref.into().unwrap_or_default(),
dyntype: K::DynamicType::default(),
}
}
}

impl<K> Ref<K>
where
K: Resource,
K::DynamicType: Default,
{
/// Construct a new Ref<K> from ObjectReference
pub fn new(object_ref: impl Into<Option<ObjectReference>>) -> Self {
let r = Self::resolve_ref(object_ref);
let r = ResourceRef::new(r);
match r.object_ref.namespace {
Some(_) => Self::Namespace(r),
None => Self::Cluster(r),
}
}

/// Perform validation of the reference against the expected resource type.
pub fn resolve_ref(object_ref: impl Into<Option<ObjectReference>>) -> Option<ObjectReference> {
let object_ref = object_ref.into()?;
let api_version = object_ref.api_version.clone()?;
let kind = object_ref.kind.clone()?;
let valid = api_version == K::api_version(&K::DynamicType::default())
&& kind == K::kind(&K::DynamicType::default());
valid.then_some(object_ref)
}
}

/// Scopes for `unstable-client` [`Client#impl-Client`] extension methods
pub mod scope {
pub use super::{Cluster, Namespace};
pub use super::{Cluster, Namespace, Ref};
}

// All objects can be listed cluster-wide
Expand Down Expand Up @@ -93,6 +158,32 @@ where
}
}

impl<K> ObjectUrl<K> for Ref<K>
where
K: Resource,
K::DynamicType: Default,
{
fn url_path(&self) -> String {
K::url_path(&K::DynamicType::default(), match self {
Ref::Namespace(ns) => ns.object_ref.namespace.as_deref(),
Ref::Cluster(_) => None,
})
}
}

impl<K> Reference<K> for Ref<K>
where
K: Resource,
K::DynamicType: Default,
{
fn name(&self) -> Option<&str> {
match self {
Ref::Namespace(ns) => ns.object_ref.name.as_deref(),
Ref::Cluster(cl) => cl.object_ref.name.as_deref(),
}
}
}

// can be created from a complete native object
impl TryFrom<&k8sNs> for Namespace {
type Error = NamespaceError;
Expand Down Expand Up @@ -184,6 +275,48 @@ impl Client {
self.request::<K>(req).await
}

/// Fetch a single instance of a `Resource` from a provided object reference.
///
/// ```no_run
/// # use k8s_openapi::api::rbac::v1::ClusterRole;
/// # use k8s_openapi::api::core::v1::Service;
/// # use k8s_openapi::api::core::v1::ObjectReference;
/// # use kube::client::scope::Ref;
/// # use kube::{ResourceExt, api::GetParams};
/// # async fn wrapper() -> Result<(), Box<dyn std::error::Error>> {
/// # let client: kube::Client = todo!();
/// let cr: ClusterRole = client.fetch(&Ref::new(ObjectReference{
/// api_version: Some("rbac.authorization.k8s.io/v1".to_string()),
/// kind: Some("ClusterRole".to_string()),
/// name: Some("cluster-admin".to_string()),
/// ..Default::default()
/// })).await?;
/// assert_eq!(cr.name_unchecked(), "cluster-admin");
/// let svc: Service = client.fetch(&Ref::new(ObjectReference{
/// api_version: Some("v1".to_string()),
/// kind: Some("Service".to_string()),
/// name: Some("kubernetes".to_string()),
/// namespace: Some("default".to_string()),
/// ..Default::default()
/// })).await?;
/// assert_eq!(svc.name_unchecked(), "kubernetes");
/// # Ok(())
/// # }
/// ```
pub async fn fetch<K>(&self, reference: &impl Reference<K>) -> Result<K>
where
K: Resource + Serialize + DeserializeOwned + Clone + Debug,
<K as Resource>::DynamicType: Default,
{
self.get(
reference
.name()
.ok_or(Error::RefResolve("Reference is empty".to_string()))?,
reference,
)
.await
}

/// List instances of a `Resource` implementing type `K` at the specified scope.
///
/// ```no_run
Expand Down Expand Up @@ -219,10 +352,11 @@ impl Client {
#[cfg(test)]
mod test {
use super::{
scope::{Cluster, Namespace},
scope::{Cluster, Namespace, Ref},
Client, ListParams,
};
use kube_core::ResourceExt;
use k8s_openapi::api::core::v1::ObjectReference;
use kube_core::{Resource, ResourceExt};

#[tokio::test]
#[ignore = "needs cluster (will list/get namespaces, pods, jobs, svcs, clusterroles)"]
Expand Down Expand Up @@ -256,4 +390,35 @@ mod test {

Ok(())
}

#[tokio::test]
#[ignore = "needs cluster (will get svcs, clusterroles)"]
async fn client_ext_fetch_ref_pods_svcs() -> Result<(), Box<dyn std::error::Error>> {
use k8s_openapi::api::{core::v1::Service, rbac::v1::ClusterRole};

let client = Client::try_default().await?;
// namespaced fetch
let svc: Service = client
.fetch(&Ref::new(ObjectReference {
kind: Some(Service::kind(&()).into()),
api_version: Some(Service::api_version(&()).into()),
name: Some("kubernetes".into()),
namespace: Some("default".into()),
..Default::default()
}))
.await?;
assert_eq!(svc.name_unchecked(), "kubernetes");
// global fetch
let ca: ClusterRole = client
.fetch(&Ref::new(ObjectReference {
kind: Some(ClusterRole::kind(&()).into()),
api_version: Some(ClusterRole::api_version(&()).into()),
name: Some("cluster-admin".into()),
..Default::default()
}))
.await?;
assert_eq!(ca.name_unchecked(), "cluster-admin");

Ok(())
}
}
6 changes: 6 additions & 0 deletions kube-client/src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,12 @@ pub enum Error {
#[cfg_attr(docsrs, doc(cfg(feature = "client")))]
#[error("auth error: {0}")]
Auth(#[source] crate::client::AuthError),

/// Error resolving resource reference
#[cfg(feature = "unstable-client")]
#[cfg_attr(docsrs, doc(cfg(feature = "unstable-client")))]
#[error("Reference resolve error: {0}")]
RefResolve(String),
}

#[derive(Error, Debug)]
Expand Down

0 comments on commit a051537

Please sign in to comment.