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
14pub trait Lookup {
18 type DynamicType;
21
22 fn kind(dyntype: &Self::DynamicType) -> Cow<'_, str>;
24
25 fn group(dyntype: &Self::DynamicType) -> Cow<'_, str>;
27
28 fn version(dyntype: &Self::DynamicType) -> Cow<'_, str>;
30
31 fn api_version(dyntype: &Self::DynamicType) -> Cow<'_, str> {
33 api_version_from_group_version(Self::group(dyntype), Self::version(dyntype))
34 }
35
36 fn plural(dyntype: &Self::DynamicType) -> Cow<'_, str>;
38
39 fn name(&self) -> Option<Cow<'_, str>>;
41
42 fn namespace(&self) -> Option<Cow<'_, str>>;
44
45 fn resource_version(&self) -> Option<Cow<'_, str>>;
47
48 fn uid(&self) -> Option<Cow<'_, str>>;
50
51 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#[non_exhaustive]
123pub struct ObjectRef<K: Lookup + ?Sized> {
124 pub dyntype: K::DynamicType,
126 pub name: String,
128 pub namespace: Option<String>,
140 #[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#[derive(Default, Debug, Clone)]
154#[non_exhaustive]
155pub struct Extra {
156 pub resource_version: Option<String>,
158 pub uid: Option<String>,
160}
161
162impl<K: Lookup> ObjectRef<K>
163where
164 K::DynamicType: Default,
165{
166 #[must_use]
168 pub fn new(name: &str) -> Self {
169 Self::new_with(name, Default::default())
170 }
171
172 #[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 #[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 #[must_use]
204 pub fn within(mut self, namespace: &str) -> Self {
205 self.namespace = Some(namespace.to_string());
206 self
207 }
208
209 #[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 #[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 #[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 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 assert_eq!(minimal, with_extra);
368
369 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}