1use std::fmt;
2
3use k8s_openapi_ext as k8s;
4use kube::api;
5use kube::discovery;
6
7use k8s::corev1;
10use 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 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 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 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}