Skip to main content

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    /// Resource information for the object K
125    pub dyntype: K::DynamicType,
126    /// The name of the object
127    pub name: String,
128    /// The namespace of the object
129    ///
130    /// May only be `None` if the kind is cluster-scoped (not located in a namespace).
131    /// Note that it *is* acceptable for an `ObjectRef` to a cluster-scoped resource to
132    /// have a namespace. These are, however, not considered equal:
133    ///
134    /// ```
135    /// # use kube_runtime::reflector::ObjectRef;
136    /// # use k8s_openapi::api::core::v1::ConfigMap;
137    /// assert_ne!(ObjectRef::<ConfigMap>::new("foo"), ObjectRef::new("foo").within("bar"));
138    /// ```
139    pub namespace: Option<String>,
140    /// Extra information about the object being referred to
141    ///
142    /// This is *not* considered when comparing objects, but may be used when converting to and from other representations,
143    /// such as [`OwnerReference`] or [`ObjectReference`].
144    #[educe(Hash(ignore), PartialEq(ignore))]
145    pub extra: Extra,
146}
147
148impl<K: Lookup + ?Sized> Eq for ObjectRef<K> where K::DynamicType: Eq {}
149
150/// Non-vital information about an object being referred to
151///
152/// See [`ObjectRef::extra`].
153#[derive(Default, Debug, Clone)]
154#[non_exhaustive]
155pub struct Extra {
156    /// The version of the resource at the time of reference
157    pub resource_version: Option<String>,
158    /// The uid of the object
159    pub uid: Option<String>,
160}
161
162impl<K: Lookup> ObjectRef<K>
163where
164    K::DynamicType: Default,
165{
166    /// Create a default object ref with a name
167    #[must_use]
168    pub fn new(name: &str) -> Self {
169        Self::new_with(name, Default::default())
170    }
171
172    /// Create an object ref from an object `K`
173    ///
174    /// This object is assumed to be valid and will expect a .metadata.name.
175    #[must_use]
176    pub fn from_obj(obj: &K) -> Self {
177        obj.to_object_ref(Default::default())
178    }
179}
180
181impl<K: Lookup> From<&K> for ObjectRef<K>
182where
183    K::DynamicType: Default,
184{
185    fn from(obj: &K) -> Self {
186        Self::from_obj(obj)
187    }
188}
189
190impl<K: Lookup> ObjectRef<K> {
191    /// Create an object ref with a name and a dynamic resource type
192    #[must_use]
193    pub fn new_with(name: &str, dyntype: K::DynamicType) -> Self {
194        Self {
195            dyntype,
196            name: name.into(),
197            namespace: None,
198            extra: Extra::default(),
199        }
200    }
201
202    /// Set the namespace on an object ref
203    #[must_use]
204    pub fn within(mut self, namespace: &str) -> Self {
205        self.namespace = Some(namespace.to_string());
206        self
207    }
208
209    /// Creates `ObjectRef` from the resource and dynamic type.
210    #[must_use]
211    pub fn from_obj_with(obj: &K, dyntype: K::DynamicType) -> Self
212    where
213        K: Lookup,
214    {
215        obj.to_object_ref(dyntype)
216    }
217
218    /// Create an `ObjectRef` from an `OwnerReference`
219    ///
220    /// Returns `None` if the types do not match.
221    #[must_use]
222    pub fn from_owner_ref(
223        namespace: Option<&str>,
224        owner: &OwnerReference,
225        dyntype: K::DynamicType,
226    ) -> Option<Self> {
227        if owner.api_version == K::api_version(&dyntype) && owner.kind == K::kind(&dyntype) {
228            Some(Self {
229                dyntype,
230                name: owner.name.clone(),
231                namespace: namespace.map(String::from),
232                extra: Extra {
233                    resource_version: None,
234                    uid: Some(owner.uid.clone()),
235                },
236            })
237        } else {
238            None
239        }
240    }
241
242    /// Convert into a reference to `K2`
243    ///
244    /// Note that no checking is done on whether this conversion makes sense. For example, every `Service`
245    /// has a corresponding `Endpoints`, but it wouldn't make sense to convert a `Pod` into a `Deployment`.
246    #[must_use]
247    pub fn into_kind_unchecked<K2: Lookup>(self, dt2: K2::DynamicType) -> ObjectRef<K2> {
248        ObjectRef {
249            dyntype: dt2,
250            name: self.name,
251            namespace: self.namespace,
252            extra: self.extra,
253        }
254    }
255
256    /// Create a object ref for a type erased dynamic object (using a static impl)
257    pub fn erase(self) -> ObjectRef<DynamicObject> {
258        ObjectRef {
259            dyntype: kube_client::api::ApiResource {
260                group: K::group(&self.dyntype).to_string(),
261                version: K::version(&self.dyntype).to_string(),
262                api_version: K::api_version(&self.dyntype).to_string(),
263                kind: K::kind(&self.dyntype).to_string(),
264                plural: K::plural(&self.dyntype).to_string(),
265            },
266            name: self.name,
267            namespace: self.namespace,
268            extra: self.extra,
269        }
270    }
271}
272
273impl<K: Lookup> From<ObjectRef<K>> for ObjectReference {
274    fn from(val: ObjectRef<K>) -> Self {
275        let ObjectRef {
276            dyntype: dt,
277            name,
278            namespace,
279            extra: Extra {
280                resource_version,
281                uid,
282            },
283        } = val;
284        ObjectReference {
285            api_version: Some(K::api_version(&dt).into_owned()),
286            kind: Some(K::kind(&dt).into_owned()),
287            field_path: None,
288            name: Some(name),
289            namespace,
290            resource_version,
291            uid,
292        }
293    }
294}
295
296impl<K: Lookup> Display for ObjectRef<K> {
297    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
298        write!(
299            f,
300            "{}.{}.{}/{}",
301            K::kind(&self.dyntype),
302            K::version(&self.dyntype),
303            K::group(&self.dyntype),
304            self.name
305        )?;
306        if let Some(namespace) = &self.namespace {
307            write!(f, ".{namespace}")?;
308        }
309        Ok(())
310    }
311}
312
313#[cfg(test)]
314mod tests {
315    use std::{
316        collections::hash_map::DefaultHasher,
317        hash::{Hash, Hasher},
318    };
319
320    use super::{Extra, ObjectRef};
321    use k8s_openapi::api::{
322        apps::v1::Deployment,
323        core::v1::{Node, Pod},
324    };
325
326    #[test]
327    fn display_should_follow_expected_format() {
328        assert_eq!(
329            format!("{}", ObjectRef::<Pod>::new("my-pod").within("my-namespace")),
330            "Pod.v1./my-pod.my-namespace"
331        );
332        assert_eq!(
333            format!(
334                "{}",
335                ObjectRef::<Deployment>::new("my-deploy").within("my-namespace")
336            ),
337            "Deployment.v1.apps/my-deploy.my-namespace"
338        );
339        assert_eq!(
340            format!("{}", ObjectRef::<Node>::new("my-node")),
341            "Node.v1./my-node"
342        );
343    }
344
345    #[test]
346    fn display_should_be_transparent_to_representation() {
347        let pod_ref = ObjectRef::<Pod>::new("my-pod").within("my-namespace");
348        assert_eq!(format!("{pod_ref}"), format!("{}", pod_ref.erase()));
349        let deploy_ref = ObjectRef::<Deployment>::new("my-deploy").within("my-namespace");
350        assert_eq!(format!("{deploy_ref}"), format!("{}", deploy_ref.erase()));
351        let node_ref = ObjectRef::<Node>::new("my-node");
352        assert_eq!(format!("{node_ref}"), format!("{}", node_ref.erase()));
353    }
354
355    #[test]
356    fn comparison_should_ignore_extra() {
357        let minimal = ObjectRef::<Pod>::new("my-pod").within("my-namespace");
358        let with_extra = ObjectRef {
359            extra: Extra {
360                resource_version: Some("123".to_string()),
361                uid: Some("638ffacd-f666-4402-ba10-7848c66ef576".to_string()),
362            },
363            ..minimal.clone()
364        };
365
366        // Eq and PartialEq should be unaffected by the contents of `extra`
367        assert_eq!(minimal, with_extra);
368
369        // Hash should be unaffected by the contents of `extra`
370        let hash_value = |value: &ObjectRef<Pod>| {
371            let mut hasher = DefaultHasher::new();
372            value.hash(&mut hasher);
373            hasher.finish()
374        };
375        assert_eq!(hash_value(&minimal), hash_value(&with_extra));
376    }
377}