rkubectl_resource/
lib.rs

1use std::fmt;
2
3use k8s_openapi_ext as k8s;
4use kube::api;
5use kube::discovery;
6
7// use k8s::authenticationv1;
8// use k8s::authorizationv1;
9use k8s::corev1;
10// use k8s::metav1;
11// use k8s::rbacv1;
12
13// use rkubectl_ext::APIResourceExt;
14use rkubectl_ext::APIResourceListExt;
15use rkubectl_kubeapi::Kubeapi;
16use rkubectl_ui::Show;
17
18pub use named::NamedResource;
19
20mod named;
21
22#[derive(Clone, Debug, PartialEq)]
23pub enum ResourceArg {
24    Resource(Resource),
25    NamedResource(NamedResource),
26}
27
28impl ResourceArg {
29    pub fn from_strings(
30        resources: &[String],
31        kubeapi: &Kubeapi,
32    ) -> Result<Vec<Self>, InvalidResourceSpec> {
33        // Two possible formats
34        // 1. resource/name - in which case all the items should be the same
35        // 2. resource[,resource,..] [name] [..]
36        if resources.iter().any(|resource| resource.contains('/')) {
37            resources
38                .iter()
39                .map(|text| Self::named_resource(text, kubeapi))
40                .collect()
41        } else {
42            let (resource, names) = resources.split_first().ok_or(InvalidResourceSpec)?;
43            let resources = resource
44                .split(",")
45                .map(|resource| Resource::with_cache(resource, kubeapi).ok_or(InvalidResourceSpec))
46                .collect::<Result<Vec<_>, _>>()?;
47            let resources = if names.is_empty() {
48                // Just resources, no names
49                resources.into_iter().map(ResourceArg::Resource).collect()
50            } else {
51                resources
52                    .into_iter()
53                    .flat_map(|resource| {
54                        names
55                            .iter()
56                            .map(move |name| NamedResource::with_resource(resource.clone(), name))
57                    })
58                    .map(Self::NamedResource)
59                    .collect()
60            };
61            Ok(resources)
62        }
63    }
64
65    fn named_resource(
66        text: impl AsRef<str>,
67        kubeapi: &Kubeapi,
68    ) -> Result<Self, InvalidResourceSpec> {
69        let (resource, name) = text.as_ref().split_once("/").ok_or(InvalidResourceSpec)?;
70        Resource::with_cache(resource, kubeapi)
71            .map(|resource| NamedResource::with_resource(resource, name))
72            .map(Self::NamedResource)
73            .ok_or(InvalidResourceSpec)
74    }
75
76    pub async fn get(&self, kubeapi: &Kubeapi) -> kube::Result<Box<dyn Show>> {
77        match self {
78            Self::Resource(resource) => resource.list(kubeapi).await,
79            Self::NamedResource(named_resource) => {
80                named_resource
81                    .resource()
82                    .get(kubeapi, named_resource.name())
83                    .await
84            }
85        }
86    }
87
88    pub async fn delete(
89        &self,
90        kubeapi: &Kubeapi,
91        dp: &api::DeleteParams,
92        all: bool,
93    ) -> kube::Result<()> {
94        match self {
95            Self::Resource(resource) if all => {
96                todo!("Deleting ALL resources {resource:?} is not implemented yet")
97            }
98            Self::Resource(resource) => {
99                todo!("Deleting SOME resources {resource:?} is not implemented yet")
100            }
101            Self::NamedResource(resource) => resource.delete(kubeapi, dp).await,
102        }
103    }
104
105    pub fn resource(&self) -> &Resource {
106        match self {
107            Self::Resource(resource) => resource,
108            Self::NamedResource(named_resource) => named_resource.resource(),
109        }
110    }
111
112    pub fn name(&self) -> Option<&str> {
113        match self {
114            Self::Resource(_resource) => None,
115            Self::NamedResource(named_resource) => Some(named_resource.name()),
116        }
117    }
118}
119
120impl fmt::Display for ResourceArg {
121    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
122        match self {
123            Self::Resource(resource) => resource.fmt(f),
124            Self::NamedResource(resource) => resource.fmt(f),
125        }
126    }
127}
128
129#[derive(Clone, Debug, PartialEq)]
130pub enum Resource {
131    Pods,
132    Namespaces,
133    Nodes,
134    ConfigMaps,
135    ComponentStatuses,
136    Other {
137        scope: discovery::Scope,
138        resource: api::ApiResource,
139    },
140}
141
142impl Resource {
143    pub fn with_cache(resource: &str, kubeapi: &Kubeapi) -> Option<Self> {
144        Self::well_known(resource).or_else(|| Self::other(resource, kubeapi))
145    }
146
147    pub fn well_known(text: &str) -> Option<Self> {
148        match text {
149            "po" | "pod" | "pods" => Some(Self::Pods),
150            "no" | "node" | "nodes" => Some(Self::Nodes),
151            "ns" | "namespace" | "namespaces" => Some(Self::Namespaces),
152            "cm" | "configmap" | "configmaps" => Some(Self::ConfigMaps),
153            "cs" | "componentstatus" | "componentstatuses" => Some(Self::ComponentStatuses),
154            _ => None,
155        }
156    }
157
158    async fn list(&self, kubeapi: &Kubeapi) -> kube::Result<Box<dyn Show>> {
159        let lp = kubeapi.list_params();
160        match self {
161            Self::Pods => {
162                let list = kubeapi.pods()?.list(&lp).await?;
163                Ok(Box::new(list))
164            }
165            Self::Namespaces => {
166                let list = kubeapi.namespaces()?.list(&lp).await?;
167                Ok(Box::new(list))
168            }
169            Self::Nodes => {
170                let list = kubeapi.nodes()?.list(&lp).await?;
171                Ok(Box::new(list))
172            }
173            Self::ConfigMaps => {
174                let list = kubeapi.configmaps()?.list(&lp).await?;
175                Ok(Box::new(list))
176            }
177            Self::ComponentStatuses => {
178                let list = kubeapi.componentstatuses()?.list(&lp).await?;
179                Ok(Box::new(list))
180            }
181            Self::Other { scope, resource } => {
182                todo!("list not implemented yet for {scope:?} {resource:?}")
183            }
184        }
185    }
186
187    async fn get(&self, kubeapi: &Kubeapi, name: &str) -> kube::Result<Box<dyn Show>> {
188        match self {
189            Self::Pods => {
190                let obj = kubeapi.pods()?.get(name).await?;
191                Ok(Box::new(obj))
192            }
193            Self::Namespaces => {
194                let obj = kubeapi.namespaces()?.get(name).await?;
195                Ok(Box::new(obj))
196            }
197            Self::Nodes => {
198                let obj = kubeapi.nodes()?.get(name).await?;
199                Ok(Box::new(obj))
200            }
201            Self::ConfigMaps => {
202                let obj = kubeapi.configmaps()?.get(name).await?;
203                Ok(Box::new(obj))
204            }
205            Self::ComponentStatuses => {
206                let obj = kubeapi.componentstatuses()?.get(name).await?;
207                Ok(Box::new(obj))
208            }
209            Self::Other { scope, resource } => {
210                todo!("get not implemented yet for {scope:?} {resource:?}")
211            }
212        }
213    }
214
215    // async fn delete(
216    //     &self,
217    //     kubectl: &Kubectl,
218    //     name: &str,
219    //     dp: &api::DeleteParams,
220    // ) -> kube::Result<()> {
221    //     let deleted = |ok| {
222    //         ok.map_left(|k| println!("{k:?}"))
223    //             .map_right(|status| println!("{status:?}"))
224    //     };
225    //     let deleted = match self {
226    //         Self::Pods => kubectl.pods()?.delete(name, dp).await.map(deleted),
227    //         Self::Namespaces => kubectl.namespaces()?.delete(name, dp).await.map(deleted),
228    //         Self::Nodes => kubectl.nodes()?.delete(name, dp).await.map(deleted),
229    //         Self::ConfigMaps => kubectl.configmaps()?.delete(name, dp).await.map(deleted),
230    //         Self::ComponentStatuses => kubectl
231    //             .componentstatuses()?
232    //             .delete(name, dp)
233    //             .await
234    //             .map(deleted),
235    //         Self::Other(resource) => {
236    //             todo!("get not implemented yet for {resource:?}")
237    //         }
238    //     };
239
240    //     Ok(())
241    // }
242
243    pub fn api_resource(&self) -> (discovery::Scope, api::ApiResource) {
244        use discovery::Scope::{Cluster, Namespaced};
245
246        match self {
247            Self::Pods => (Namespaced, Self::erase::<corev1::Pod>()),
248            Self::Namespaces => (Cluster, Self::erase::<corev1::Namespace>()),
249            Self::Nodes => (Cluster, Self::erase::<corev1::Node>()),
250            Self::ConfigMaps => (Namespaced, Self::erase::<corev1::ConfigMap>()),
251            Self::ComponentStatuses => (Namespaced, Self::erase::<corev1::ComponentStatus>()),
252            Self::Other { scope, resource } => (scope.clone(), resource.clone()),
253        }
254    }
255
256    fn cached_dynamic_api_resource(
257        kubeapi: &Kubeapi,
258        name: &str,
259    ) -> Option<(discovery::Scope, api::ApiResource)> {
260        kubeapi
261            .cached_server_api_resources()
262            .into_iter()
263            .find_map(|arl| arl.kube_api_resource(name))
264    }
265
266    fn erase<K>() -> api::ApiResource
267    where
268        K: kube::Resource,
269        <K as kube::Resource>::DynamicType: Default,
270    {
271        api::ApiResource::erase::<K>(&<K as kube::Resource>::DynamicType::default())
272    }
273
274    fn other(resource: &str, kubeapi: &Kubeapi) -> Option<Self> {
275        Self::cached_dynamic_api_resource(kubeapi, resource)
276            .map(|(scope, resource)| Self::Other { scope, resource })
277    }
278}
279
280impl fmt::Display for Resource {
281    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
282        match self {
283            Self::Pods => "pod".fmt(f),
284            Self::Namespaces => "namespace".fmt(f),
285            Self::Nodes => "node".fmt(f),
286            Self::ConfigMaps => "configmap".fmt(f),
287            Self::ComponentStatuses => "componentstatus".fmt(f),
288            Self::Other { resource, .. } => resource.kind.to_lowercase().fmt(f),
289        }
290    }
291}
292
293#[derive(Debug, thiserror::Error)]
294#[error(
295    "there is no need to specify a resource type as a separate argument when passing arguments in resource/name form (e.g. 'kubectl get resource/<resource_name>' instead of 'kubectl get resource resource/<resource_name>')"
296)]
297pub struct InvalidResourceSpec;
298
299#[cfg(test)]
300mod tests {
301    use super::*;
302
303    fn args(s: &[&str]) -> Result<Vec<ResourceArg>, InvalidResourceSpec> {
304        let resources = s.iter().map(ToString::to_string).collect::<Vec<_>>();
305        let kubeapi = Kubeapi::local();
306        ResourceArg::from_strings(&resources, &kubeapi)
307    }
308
309    #[test]
310    fn one_resource() {
311        let resources = args(&["pod"]).unwrap();
312        assert_eq!(resources.len(), 1);
313        assert_eq!(resources[0], ResourceArg::Resource(Resource::Pods));
314    }
315
316    #[test]
317    fn many_resources() {
318        let resources = args(&["pod,node"]).unwrap();
319        assert_eq!(resources.len(), 2);
320
321        let ResourceArg::Resource(ref pod) = resources[0] else {
322            panic!("expecting NamedResource, found something else");
323        };
324        let ResourceArg::Resource(ref node) = resources[1] else {
325            panic!("expecting NamedResource, found something else");
326        };
327
328        assert_eq!(pod, &Resource::Pods);
329        assert_eq!(node, &Resource::Nodes);
330    }
331
332    #[test]
333    fn resource_and_name() {
334        let resources = args(&["pod", "bazooka"]).unwrap();
335        assert_eq!(resources.len(), 1);
336        let ResourceArg::NamedResource(ref pod) = resources[0] else {
337            panic!("expecting NamedResource, found something else");
338        };
339        assert_eq!(pod.resource(), &Resource::Pods);
340        assert_eq!(pod.name(), "bazooka");
341    }
342
343    #[test]
344    fn resource_and_many_name() {
345        let resources = args(&["pod", "bazooka", "darbooka"]).unwrap();
346        assert_eq!(resources.len(), 2);
347        let ResourceArg::NamedResource(ref pod1) = resources[0] else {
348            panic!("expecting NamedResource, found something else");
349        };
350        let ResourceArg::NamedResource(ref pod2) = resources[1] else {
351            panic!("expecting NamedResource, found something else");
352        };
353        assert_eq!(pod1.resource(), &Resource::Pods);
354        assert_eq!(pod1.name(), "bazooka");
355        assert_eq!(pod2.resource(), &Resource::Pods);
356        assert_eq!(pod2.name(), "darbooka");
357    }
358
359    #[test]
360    fn one_named_resource() {
361        let resources = args(&["pod/bazooka"]).unwrap();
362        assert_eq!(resources.len(), 1);
363        let ResourceArg::NamedResource(ref pod) = resources[0] else {
364            panic!("expecting NamedResource, found something else");
365        };
366
367        assert_eq!(pod.resource(), &Resource::Pods);
368        assert_eq!(pod.name(), "bazooka");
369    }
370
371    #[test]
372    fn many_named_resources() {
373        let resources = args(&["pod/bazooka", "node/elephant"]).unwrap();
374        assert_eq!(resources.len(), 2);
375
376        let ResourceArg::NamedResource(ref pod) = resources[0] else {
377            panic!("expecting NamedResource, found something else");
378        };
379        let ResourceArg::NamedResource(ref node) = resources[1] else {
380            panic!("expecting NamedResource, found something else");
381        };
382
383        assert_eq!(pod.resource(), &Resource::Pods);
384        assert_eq!(pod.name(), "bazooka");
385        assert_eq!(node.resource(), &Resource::Nodes);
386        assert_eq!(node.name(), "elephant");
387    }
388
389    #[test]
390    fn invalid_mix() {
391        let _err = args(&["pod/bazooka", "node"]).unwrap_err();
392    }
393}