use derivative::Derivative;
use k8s_openapi::{apimachinery::pkg::apis::meta::v1::OwnerReference, Resource};
use kube::api::Meta;
use std::{
fmt::{Debug, Display},
hash::Hash,
};
#[derive(Derivative)]
#[derivative(Debug, PartialEq, Eq, Hash, Clone)]
pub struct ObjectRef<K: RuntimeResource> {
kind: K::State,
pub name: String,
pub namespace: Option<String>,
}
impl<K: Resource> ObjectRef<K> {
#[must_use]
pub fn new(name: &str) -> Self {
Self {
kind: (),
name: name.into(),
namespace: None,
}
}
#[must_use]
pub fn within(mut self, namespace: &str) -> Self {
self.namespace = Some(namespace.to_string());
self
}
#[must_use]
pub fn from_obj(obj: &K) -> Self
where
K: Meta,
{
Self {
kind: (),
name: obj.name(),
namespace: obj.namespace(),
}
}
#[must_use]
pub fn from_owner_ref(namespace: Option<&str>, owner: &OwnerReference) -> Option<Self> {
if owner.api_version == K::API_VERSION && owner.kind == K::KIND {
Some(Self {
kind: (),
name: owner.name.clone(),
namespace: namespace.map(String::from),
})
} else {
None
}
}
#[must_use]
pub fn into_kind_unchecked<K2: Resource>(self) -> ObjectRef<K2> {
ObjectRef {
kind: (),
name: self.name,
namespace: self.namespace,
}
}
}
impl<K: RuntimeResource> Display for ObjectRef<K> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
f,
"{}.{}.{}/{}",
K::kind(&self.kind),
K::version(&self.kind),
K::group(&self.kind),
self.name
)?;
if let Some(namespace) = &self.namespace {
write!(f, ".{}", namespace)?;
}
Ok(())
}
}
pub trait RuntimeResource {
type State: Debug + PartialEq + Eq + Hash + Clone;
fn group(state: &Self::State) -> &str;
fn version(state: &Self::State) -> &str;
fn kind(state: &Self::State) -> &str;
}
impl<K: Resource> RuntimeResource for K {
type State = ();
fn group(_state: &Self::State) -> &str {
K::GROUP
}
fn version(_state: &Self::State) -> &str {
K::VERSION
}
fn kind(_state: &Self::State) -> &str {
K::KIND
}
}
#[allow(clippy::empty_enum)]
pub enum ErasedResource {}
#[derive(Debug, Clone, Copy, Hash, PartialEq, Eq)]
pub struct ErasedResourceState {
group: &'static str,
version: &'static str,
kind: &'static str,
}
impl RuntimeResource for ErasedResource {
type State = ErasedResourceState;
fn group(state: &Self::State) -> &str {
&state.group
}
fn version(state: &Self::State) -> &str {
&state.version
}
fn kind(state: &Self::State) -> &str {
&state.kind
}
}
impl ErasedResource {
fn erase<K: Resource>() -> ErasedResourceState {
ErasedResourceState {
group: K::GROUP,
version: K::VERSION,
kind: K::KIND,
}
}
}
impl<K: Resource> From<ObjectRef<K>> for ObjectRef<ErasedResource> {
fn from(old: ObjectRef<K>) -> Self {
ObjectRef {
kind: ErasedResource::erase::<K>(),
name: old.name,
namespace: old.namespace,
}
}
}
#[cfg(test)]
mod tests {
use super::{ErasedResource, ObjectRef};
use k8s_openapi::api::{
apps::v1::Deployment,
core::v1::{Node, Pod},
};
#[test]
fn display_should_follow_expected_format() {
assert_eq!(
format!("{}", ObjectRef::<Pod>::new("my-pod").within("my-namespace")),
"Pod.v1./my-pod.my-namespace"
);
assert_eq!(
format!(
"{}",
ObjectRef::<Deployment>::new("my-deploy").within("my-namespace")
),
"Deployment.v1.apps/my-deploy.my-namespace"
);
assert_eq!(
format!("{}", ObjectRef::<Node>::new("my-node")),
"Node.v1./my-node"
);
}
#[test]
fn display_should_be_transparent_to_representation() {
let pod_ref = ObjectRef::<Pod>::new("my-pod").within("my-namespace");
assert_eq!(
format!("{}", pod_ref),
format!("{}", ObjectRef::<ErasedResource>::from(pod_ref))
);
let deploy_ref = ObjectRef::<Deployment>::new("my-deploy").within("my-namespace");
assert_eq!(
format!("{}", deploy_ref),
format!("{}", ObjectRef::<ErasedResource>::from(deploy_ref))
);
let node_ref = ObjectRef::<Node>::new("my-node");
assert_eq!(
format!("{}", node_ref),
format!("{}", ObjectRef::<ErasedResource>::from(node_ref))
);
}
}