1use crate::{Client, Error, Result};
2use k8s_openapi::{
3 api::core::v1::{LocalObjectReference, Namespace as k8sNs, ObjectReference},
4 apimachinery::pkg::apis::meta::v1::OwnerReference,
5};
6use kube_core::{
7 object::ObjectList,
8 params::{GetParams, ListParams},
9 request::Request,
10 ApiResource, ClusterResourceScope, DynamicResourceScope, NamespaceResourceScope, Resource,
11};
12use serde::{de::DeserializeOwned, Serialize};
13use std::fmt::Debug;
14
15trait ClusterScope {}
17trait NamespaceScope {}
19
20impl ClusterScope for ClusterResourceScope {}
22impl NamespaceScope for NamespaceResourceScope {}
23impl NamespaceScope for DynamicResourceScope {}
25impl ClusterScope for DynamicResourceScope {}
26
27pub trait CollectionUrl<K> {
31 fn url_path(&self) -> String;
32}
33
34pub trait ObjectUrl<K> {
38 fn url_path(&self) -> String;
39}
40
41#[derive(Debug, Clone)]
43pub struct Cluster;
44#[derive(Debug, Clone)]
48pub struct Namespace(String);
49
50pub trait ObjectRef<K>: ObjectUrl<K> {
52 fn name(&self) -> Option<&str>;
53}
54
55pub trait NamespacedRef<K> {
57 fn within(&self, namespace: impl Into<Option<String>>) -> impl ObjectRef<K>;
59}
60
61impl<K> ObjectUrl<K> for ObjectReference
62where
63 K: Resource,
64{
65 fn url_path(&self) -> String {
66 url_path(
67 &ApiResource::from_gvk(&self.clone().into()),
68 self.namespace.clone(),
69 )
70 }
71}
72
73impl<K> ObjectRef<K> for ObjectReference
74where
75 K: Resource,
76{
77 fn name(&self) -> Option<&str> {
78 self.name.as_deref()
79 }
80}
81
82impl<K> NamespacedRef<K> for ObjectReference
83where
84 K: Resource,
85 K::Scope: NamespaceScope,
86{
87 fn within(&self, namespace: impl Into<Option<String>>) -> impl ObjectRef<K> {
88 Self {
89 namespace: namespace.into(),
90 ..self.clone()
91 }
92 }
93}
94
95impl<K> ObjectUrl<K> for OwnerReference
96where
97 K: Resource,
98 K::Scope: ClusterScope,
99{
100 fn url_path(&self) -> String {
101 url_path(&ApiResource::from_gvk(&self.clone().into()), None)
102 }
103}
104
105impl<K> ObjectRef<K> for OwnerReference
106where
107 K: Resource,
108 K::Scope: ClusterScope,
109{
110 fn name(&self) -> Option<&str> {
111 self.name.as_str().into()
112 }
113}
114
115impl<K> NamespacedRef<K> for OwnerReference
116where
117 K: Resource,
118 K::Scope: NamespaceScope,
119{
120 fn within(&self, namespace: impl Into<Option<String>>) -> impl ObjectRef<K> {
121 ObjectReference {
122 api_version: self.api_version.clone().into(),
123 namespace: namespace.into(),
124 name: self.name.clone().into(),
125 uid: self.uid.clone().into(),
126 kind: self.kind.clone().into(),
127 ..Default::default()
128 }
129 }
130}
131
132impl<K> NamespacedRef<K> for LocalObjectReference
133where
134 K: Resource,
135 K::DynamicType: Default,
136 K::Scope: NamespaceScope,
137{
138 fn within(&self, namespace: impl Into<Option<String>>) -> impl ObjectRef<K> {
139 let dt = Default::default();
140 ObjectReference {
141 api_version: K::api_version(&dt).to_string().into(),
142 namespace: namespace.into(),
143 name: Some(self.name.clone()),
144 kind: K::kind(&dt).to_string().into(),
145 ..Default::default()
146 }
147 }
148}
149
150pub mod scope {
152 pub use super::{Cluster, Namespace, NamespacedRef};
153}
154
155impl<K> CollectionUrl<K> for Cluster
157where
158 K: Resource,
159 K::DynamicType: Default,
160{
161 fn url_path(&self) -> String {
162 K::url_path(&K::DynamicType::default(), None)
163 }
164}
165
166impl<K> ObjectUrl<K> for Cluster
168where
169 K: Resource,
170 K::DynamicType: Default,
171 K::Scope: ClusterScope,
172{
173 fn url_path(&self) -> String {
174 K::url_path(&K::DynamicType::default(), None)
175 }
176}
177
178impl<K> CollectionUrl<K> for Namespace
180where
181 K: Resource,
182 K::DynamicType: Default,
183 K::Scope: NamespaceScope,
184{
185 fn url_path(&self) -> String {
186 K::url_path(&K::DynamicType::default(), Some(&self.0))
187 }
188}
189
190impl<K> ObjectUrl<K> for Namespace
191where
192 K: Resource,
193 K::DynamicType: Default,
194 K::Scope: NamespaceScope,
195{
196 fn url_path(&self) -> String {
197 K::url_path(&K::DynamicType::default(), Some(&self.0))
198 }
199}
200
201impl TryFrom<&k8sNs> for Namespace {
203 type Error = NamespaceError;
204
205 fn try_from(ns: &k8sNs) -> Result<Namespace, Self::Error> {
206 if let Some(n) = &ns.meta().name {
207 Ok(Namespace(n.to_owned()))
208 } else {
209 Err(NamespaceError::MissingName)
210 }
211 }
212}
213impl From<&str> for Namespace {
215 fn from(ns: &str) -> Namespace {
216 Namespace(ns.to_owned())
217 }
218}
219impl From<String> for Namespace {
220 fn from(ns: String) -> Namespace {
221 Namespace(ns)
222 }
223}
224
225#[derive(thiserror::Error, Debug)]
226pub enum NamespaceError {
228 #[error("Missing Namespace Name")]
230 MissingName,
231}
232
233impl Client {
264 pub async fn get<K>(&self, name: &str, scope: &impl ObjectUrl<K>) -> Result<K>
282 where
283 K: Resource + Serialize + DeserializeOwned + Clone + Debug,
284 <K as Resource>::DynamicType: Default,
285 {
286 let mut req = Request::new(scope.url_path())
287 .get(name, &GetParams::default())
288 .map_err(Error::BuildRequest)?;
289 req.extensions_mut().insert("get");
290 self.request::<K>(req).await
291 }
292
293 pub async fn fetch<K>(&self, reference: &impl ObjectRef<K>) -> Result<K>
340 where
341 K: Resource + Serialize + DeserializeOwned + Clone + Debug,
342 {
343 let mut req = Request::new(reference.url_path())
344 .get(
345 reference
346 .name()
347 .ok_or(Error::RefResolve("Reference is empty".to_string()))?,
348 &GetParams::default(),
349 )
350 .map_err(Error::BuildRequest)?;
351 req.extensions_mut().insert("get");
352 self.request::<K>(req).await
353 }
354
355 pub async fn list<K>(&self, lp: &ListParams, scope: &impl CollectionUrl<K>) -> Result<ObjectList<K>>
376 where
377 K: Resource + Serialize + DeserializeOwned + Clone + Debug,
378 <K as Resource>::DynamicType: Default,
379 {
380 let mut req = Request::new(scope.url_path())
381 .list(lp)
382 .map_err(Error::BuildRequest)?;
383 req.extensions_mut().insert("list");
384 self.request::<ObjectList<K>>(req).await
385 }
386}
387
388fn url_path(r: &ApiResource, namespace: Option<String>) -> String {
390 let n = if let Some(ns) = namespace {
391 format!("namespaces/{ns}/")
392 } else {
393 "".into()
394 };
395 format!(
396 "/{group}/{api_version}/{namespaces}{plural}",
397 group = if r.group.is_empty() { "api" } else { "apis" },
398 api_version = r.api_version,
399 namespaces = n,
400 plural = r.plural
401 )
402}
403
404#[cfg(test)]
405mod test {
406 use crate::{
407 client::{
408 client_ext::NamespacedRef as _,
409 scope::{Cluster, Namespace},
410 },
411 Client,
412 };
413
414 use super::ListParams;
415 use k8s_openapi::api::core::v1::LocalObjectReference;
416 use kube_core::{DynamicObject, Resource as _, ResourceExt as _};
417
418 #[tokio::test]
419 #[ignore = "needs cluster (will list/get namespaces, pods, jobs, svcs, clusterroles)"]
420 async fn client_ext_list_get_pods_svcs() -> Result<(), Box<dyn std::error::Error>> {
421 use k8s_openapi::api::{
422 batch::v1::Job,
423 core::v1::{Namespace as k8sNs, Pod, Service},
424 rbac::v1::ClusterRole,
425 };
426
427 let client = Client::try_default().await?;
428 let lp = ListParams::default();
429 for ns in client.list::<k8sNs>(&lp, &Cluster).await? {
431 for p in client.list::<Pod>(&lp, &Namespace::try_from(&ns)?).await? {
433 println!("Found pod {} in {}", p.name_any(), ns.name_any());
434 }
435 }
436 for j in client.list::<Job>(&lp, &Cluster).await? {
438 println!("Found job {} in {}", j.name_any(), j.namespace().unwrap());
439 }
440 let default: Namespace = "default".into();
442 let svc = client.get::<Service>("kubernetes", &default).await?;
443 assert_eq!(svc.name_unchecked(), "kubernetes");
444 let ca = client.get::<ClusterRole>("cluster-admin", &Cluster).await?;
446 assert_eq!(ca.name_unchecked(), "cluster-admin");
447
448 Ok(())
449 }
450
451 #[tokio::test]
452 #[ignore = "needs cluster (will get svcs, clusterroles, pods, nodes)"]
453 async fn client_ext_fetch_ref_pods_svcs() -> Result<(), Box<dyn std::error::Error>> {
454 use k8s_openapi::api::{
455 core::v1::{Node, ObjectReference, Pod, Service},
456 rbac::v1::ClusterRole,
457 };
458
459 let client = Client::try_default().await?;
460 let svc: Service = client
462 .fetch(&ObjectReference {
463 kind: Some(Service::kind(&()).into()),
464 api_version: Some(Service::api_version(&()).into()),
465 name: Some("kubernetes".into()),
466 namespace: Some("default".into()),
467 ..Default::default()
468 })
469 .await?;
470 assert_eq!(svc.name_unchecked(), "kubernetes");
471 let ca: ClusterRole = client
473 .fetch(&ObjectReference {
474 kind: Some(ClusterRole::kind(&()).into()),
475 api_version: Some(ClusterRole::api_version(&()).into()),
476 name: Some("cluster-admin".into()),
477 ..Default::default()
478 })
479 .await?;
480 assert_eq!(ca.name_unchecked(), "cluster-admin");
481 let svc: DynamicObject = client.fetch(&svc.object_ref(&())).await?;
483 assert_eq!(svc.name_unchecked(), "kubernetes");
484 let ca: DynamicObject = client.fetch(&ca.object_ref(&())).await?;
486 assert_eq!(ca.name_unchecked(), "cluster-admin");
487
488 let svc: Service = client
490 .fetch(
491 &LocalObjectReference {
492 name: svc.name_any().into(),
493 }
494 .within(svc.namespace()),
495 )
496 .await?;
497 assert_eq!(svc.name_unchecked(), "kubernetes");
498
499 let kube_system: Namespace = "kube-system".into();
500 for pod in client
501 .list::<Pod>(
502 &ListParams::default().labels("component=kube-apiserver"),
503 &kube_system,
504 )
505 .await?
506 {
507 let owner = pod
508 .owner_references()
509 .iter()
510 .find(|r| r.kind == Node::kind(&()))
511 .ok_or("Not found")?;
512 let _: Node = client.fetch(owner).await?;
513 }
514
515 Ok(())
516 }
517
518 #[tokio::test]
519 #[ignore = "needs cluster (will get svcs, clusterroles, pods, nodes)"]
520 async fn fetch_fails() -> Result<(), Box<dyn std::error::Error>> {
521 use crate::error::Error;
522 use k8s_openapi::api::core::v1::{ObjectReference, Pod, Service};
523
524 let client = Client::try_default().await?;
525 let svc: Service = client
527 .fetch(&ObjectReference {
528 kind: Some(Service::kind(&()).into()),
529 api_version: Some(Service::api_version(&()).into()),
530 name: Some("kubernetes".into()),
531 namespace: Some("default".into()),
532 ..Default::default()
533 })
534 .await?;
535 let err = client.fetch::<Pod>(&svc.object_ref(&())).await.unwrap_err();
536 assert!(matches!(err, Error::SerdeError(_)));
537 assert_eq!(err.to_string(), "Error deserializing response: invalid value: string \"Service\", expected Pod at line 1 column 17".to_string());
538
539 let obj: DynamicObject = client.fetch(&svc.object_ref(&())).await?;
540 let err = obj.try_parse::<Pod>().unwrap_err();
541 assert_eq!(err.to_string(), "failed to parse this DynamicObject into a Resource: invalid value: string \"Service\", expected Pod".to_string());
542
543 Ok(())
544 }
545}