kube_runtime/reflector/
object_ref.rs

1use educe::Educe;
2use k8s_openapi::{api::core::v1::ObjectReference, apimachinery::pkg::apis::meta::v1::OwnerReference};
3#[cfg(doc)] use kube_client::core::ObjectMeta;
4use kube_client::{
5    api::{DynamicObject, Resource},
6    core::api_version_from_group_version,
7};
8use std::{
9    borrow::Cow,
10    fmt::{Debug, Display},
11    hash::Hash,
12};
13
14/// Minimal lookup behaviour needed by a [reflector store](super::Store).
15///
16/// This trait is blanket-implemented for all [`Resource`] objects.
17pub trait Lookup {
18    /// Type information for types that do not know their resource information at compile time.
19    /// This is equivalent to [`Resource::DynamicType`].
20    type DynamicType;
21
22    /// The [kind](Resource::kind) for this object.
23    fn kind(dyntype: &Self::DynamicType) -> Cow<'_, str>;
24
25    /// The [group](Resource::group) for this object.
26    fn group(dyntype: &Self::DynamicType) -> Cow<'_, str>;
27
28    /// The [version](Resource::version) for this object.
29    fn version(dyntype: &Self::DynamicType) -> Cow<'_, str>;
30
31    /// The [apiVersion](Resource::_version) for this object.
32    fn api_version(dyntype: &Self::DynamicType) -> Cow<'_, str> {
33        api_version_from_group_version(Self::group(dyntype), Self::version(dyntype))
34    }
35
36    /// The [plural](Resource::plural) for this object.
37    fn plural(dyntype: &Self::DynamicType) -> Cow<'_, str>;
38
39    /// The [name](ObjectMeta#structfield.name) of the object.
40    fn name(&self) -> Option<Cow<'_, str>>;
41
42    /// The [namespace](ObjectMeta#structfield.namespace) of the object.
43    fn namespace(&self) -> Option<Cow<'_, str>>;
44
45    /// The [resource version](ObjectMeta#structfield.resource_version) of the object.
46    fn resource_version(&self) -> Option<Cow<'_, str>>;
47
48    /// The [UID](ObjectMeta#structfield.uid) of the object.
49    fn uid(&self) -> Option<Cow<'_, str>>;
50
51    /// Constructs an [`ObjectRef`] for this object.
52    fn to_object_ref(&self, dyntype: Self::DynamicType) -> ObjectRef<Self> {
53        ObjectRef {
54            dyntype,
55            name: self.name().expect(".metadata.name missing").into_owned(),
56            namespace: self.namespace().map(Cow::into_owned),
57            extra: Extra {
58                resource_version: self.resource_version().map(Cow::into_owned),
59                uid: self.uid().map(Cow::into_owned),
60            },
61        }
62    }
63}
64
65impl<K: Resource> Lookup for K {
66    type DynamicType = K::DynamicType;
67
68    fn kind(dyntype: &Self::DynamicType) -> Cow<'_, str> {
69        K::kind(dyntype)
70    }
71
72    fn version(dyntype: &Self::DynamicType) -> Cow<'_, str> {
73        K::version(dyntype)
74    }
75
76    fn group(dyntype: &Self::DynamicType) -> Cow<'_, str> {
77        K::group(dyntype)
78    }
79
80    fn plural(dyntype: &Self::DynamicType) -> Cow<'_, str> {
81        K::plural(dyntype)
82    }
83
84    fn name(&self) -> Option<Cow<'_, str>> {
85        self.meta().name.as_deref().map(Cow::Borrowed)
86    }
87
88    fn namespace(&self) -> Option<Cow<'_, str>> {
89        self.meta().namespace.as_deref().map(Cow::Borrowed)
90    }
91
92    fn resource_version(&self) -> Option<Cow<'_, str>> {
93        self.meta().resource_version.as_deref().map(Cow::Borrowed)
94    }
95
96    fn uid(&self) -> Option<Cow<'_, str>> {
97        self.meta().uid.as_deref().map(Cow::Borrowed)
98    }
99}
100
101#[derive(Educe)]
102#[educe(
103    Debug(bound("K::DynamicType: Debug")),
104    PartialEq(bound("K::DynamicType: PartialEq")),
105    Hash(bound("K::DynamicType: Hash")),
106    Clone(bound("K::DynamicType: Clone"))
107)]
108/// A typed and namedspaced (if relevant) reference to a Kubernetes object
109///
110/// `K` may be either the object type or `DynamicObject`, in which case the
111/// type is stored at runtime. Erased `ObjectRef`s pointing to different types
112/// are still considered different.
113///
114/// ```
115/// use kube_runtime::reflector::ObjectRef;
116/// use k8s_openapi::api::core::v1::{ConfigMap, Secret};
117/// assert_ne!(
118///     ObjectRef::<ConfigMap>::new("a").erase(),
119///     ObjectRef::<Secret>::new("a").erase(),
120/// );
121/// ```
122#[non_exhaustive]
123pub struct ObjectRef<K: Lookup + ?Sized> {
124    pub dyntype: K::DynamicType,
125    /// The name of the object
126    pub name: String,
127    /// The namespace of the object
128    ///
129    /// May only be `None` if the kind is cluster-scoped (not located in a namespace).
130    /// Note that it *is* acceptable for an `ObjectRef` to a cluster-scoped resource to
131    /// have a namespace. These are, however, not considered equal:
132    ///
133    /// ```
134    /// # use kube_runtime::reflector::ObjectRef;
135    /// # use k8s_openapi::api::core::v1::ConfigMap;
136    /// assert_ne!(ObjectRef::<ConfigMap>::new("foo"), ObjectRef::new("foo").within("bar"));
137    /// ```
138    pub namespace: Option<String>,
139    /// Extra information about the object being referred to
140    ///
141    /// This is *not* considered when comparing objects, but may be used when converting to and from other representations,
142    /// such as [`OwnerReference`] or [`ObjectReference`].
143    #[educe(Hash(ignore), PartialEq(ignore))]
144    pub extra: Extra,
145}
146
147impl<K: Lookup + ?Sized> Eq for ObjectRef<K> where K::DynamicType: Eq {}
148
149/// Non-vital information about an object being referred to
150///
151/// See [`ObjectRef::extra`].
152#[derive(Default, Debug, Clone)]
153#[non_exhaustive]
154pub struct Extra {
155    /// The version of the resource at the time of reference
156    pub resource_version: Option<String>,
157    /// The uid of the object
158    pub uid: Option<String>,
159}
160
161impl<K: Lookup> ObjectRef<K>
162where
163    K::DynamicType: Default,
164{
165    #[must_use]
166    pub fn new(name: &str) -> Self {
167        Self::new_with(name, Default::default())
168    }
169
170    #[must_use]
171    pub fn from_obj(obj: &K) -> Self {
172        obj.to_object_ref(Default::default())
173    }
174}
175
176impl<K: Lookup> From<&K> for ObjectRef<K>
177where
178    K::DynamicType: Default,
179{
180    fn from(obj: &K) -> Self {
181        Self::from_obj(obj)
182    }
183}
184
185impl<K: Lookup> ObjectRef<K> {
186    #[must_use]
187    pub fn new_with(name: &str, dyntype: K::DynamicType) -> Self {
188        Self {
189            dyntype,
190            name: name.into(),
191            namespace: None,
192            extra: Extra::default(),
193        }
194    }
195
196    #[must_use]
197    pub fn within(mut self, namespace: &str) -> Self {
198        self.namespace = Some(namespace.to_string());
199        self
200    }
201
202    /// Creates `ObjectRef` from the resource and dynamic type.
203    #[must_use]
204    pub fn from_obj_with(obj: &K, dyntype: K::DynamicType) -> Self
205    where
206        K: Lookup,
207    {
208        obj.to_object_ref(dyntype)
209    }
210
211    /// Create an `ObjectRef` from an `OwnerReference`
212    ///
213    /// Returns `None` if the types do not match.
214    #[must_use]
215    pub fn from_owner_ref(
216        namespace: Option<&str>,
217        owner: &OwnerReference,
218        dyntype: K::DynamicType,
219    ) -> Option<Self> {
220        if owner.api_version == K::api_version(&dyntype) && owner.kind == K::kind(&dyntype) {
221            Some(Self {
222                dyntype,
223                name: owner.name.clone(),
224                namespace: namespace.map(String::from),
225                extra: Extra {
226                    resource_version: None,
227                    uid: Some(owner.uid.clone()),
228                },
229            })
230        } else {
231            None
232        }
233    }
234
235    /// Convert into a reference to `K2`
236    ///
237    /// Note that no checking is done on whether this conversion makes sense. For example, every `Service`
238    /// has a corresponding `Endpoints`, but it wouldn't make sense to convert a `Pod` into a `Deployment`.
239    #[must_use]
240    pub fn into_kind_unchecked<K2: Lookup>(self, dt2: K2::DynamicType) -> ObjectRef<K2> {
241        ObjectRef {
242            dyntype: dt2,
243            name: self.name,
244            namespace: self.namespace,
245            extra: self.extra,
246        }
247    }
248
249    pub fn erase(self) -> ObjectRef<DynamicObject> {
250        ObjectRef {
251            dyntype: kube_client::api::ApiResource {
252                group: K::group(&self.dyntype).to_string(),
253                version: K::version(&self.dyntype).to_string(),
254                api_version: K::api_version(&self.dyntype).to_string(),
255                kind: K::kind(&self.dyntype).to_string(),
256                plural: K::plural(&self.dyntype).to_string(),
257            },
258            name: self.name,
259            namespace: self.namespace,
260            extra: self.extra,
261        }
262    }
263}
264
265impl<K: Lookup> From<ObjectRef<K>> for ObjectReference {
266    fn from(val: ObjectRef<K>) -> Self {
267        let ObjectRef {
268            dyntype: dt,
269            name,
270            namespace,
271            extra: Extra {
272                resource_version,
273                uid,
274            },
275        } = val;
276        ObjectReference {
277            api_version: Some(K::api_version(&dt).into_owned()),
278            kind: Some(K::kind(&dt).into_owned()),
279            field_path: None,
280            name: Some(name),
281            namespace,
282            resource_version,
283            uid,
284        }
285    }
286}
287
288impl<K: Lookup> Display for ObjectRef<K> {
289    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
290        write!(
291            f,
292            "{}.{}.{}/{}",
293            K::kind(&self.dyntype),
294            K::version(&self.dyntype),
295            K::group(&self.dyntype),
296            self.name
297        )?;
298        if let Some(namespace) = &self.namespace {
299            write!(f, ".{namespace}")?;
300        }
301        Ok(())
302    }
303}
304
305#[cfg(test)]
306mod tests {
307    use std::{
308        collections::hash_map::DefaultHasher,
309        hash::{Hash, Hasher},
310    };
311
312    use super::{Extra, ObjectRef};
313    use k8s_openapi::api::{
314        apps::v1::Deployment,
315        core::v1::{Node, Pod},
316    };
317
318    #[test]
319    fn display_should_follow_expected_format() {
320        assert_eq!(
321            format!("{}", ObjectRef::<Pod>::new("my-pod").within("my-namespace")),
322            "Pod.v1./my-pod.my-namespace"
323        );
324        assert_eq!(
325            format!(
326                "{}",
327                ObjectRef::<Deployment>::new("my-deploy").within("my-namespace")
328            ),
329            "Deployment.v1.apps/my-deploy.my-namespace"
330        );
331        assert_eq!(
332            format!("{}", ObjectRef::<Node>::new("my-node")),
333            "Node.v1./my-node"
334        );
335    }
336
337    #[test]
338    fn display_should_be_transparent_to_representation() {
339        let pod_ref = ObjectRef::<Pod>::new("my-pod").within("my-namespace");
340        assert_eq!(format!("{pod_ref}"), format!("{}", pod_ref.erase()));
341        let deploy_ref = ObjectRef::<Deployment>::new("my-deploy").within("my-namespace");
342        assert_eq!(format!("{deploy_ref}"), format!("{}", deploy_ref.erase()));
343        let node_ref = ObjectRef::<Node>::new("my-node");
344        assert_eq!(format!("{node_ref}"), format!("{}", node_ref.erase()));
345    }
346
347    #[test]
348    fn comparison_should_ignore_extra() {
349        let minimal = ObjectRef::<Pod>::new("my-pod").within("my-namespace");
350        let with_extra = ObjectRef {
351            extra: Extra {
352                resource_version: Some("123".to_string()),
353                uid: Some("638ffacd-f666-4402-ba10-7848c66ef576".to_string()),
354            },
355            ..minimal.clone()
356        };
357
358        // Eq and PartialEq should be unaffected by the contents of `extra`
359        assert_eq!(minimal, with_extra);
360
361        // Hash should be unaffected by the contents of `extra`
362        let hash_value = |value: &ObjectRef<Pod>| {
363            let mut hasher = DefaultHasher::new();
364            value.hash(&mut hasher);
365            hasher.finish()
366        };
367        assert_eq!(hash_value(&minimal), hash_value(&with_extra));
368    }
369}