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,
125 pub name: String,
127 pub namespace: Option<String>,
139 #[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#[derive(Default, Debug, Clone)]
153#[non_exhaustive]
154pub struct Extra {
155 pub resource_version: Option<String>,
157 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 #[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 #[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 #[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 assert_eq!(minimal, with_extra);
360
361 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}