From 943a98075cd8d005e4557dd58e8e9acbcd164a2f Mon Sep 17 00:00:00 2001 From: Shinu Suresh Date: Tue, 12 Jul 2022 18:28:20 +0100 Subject: [PATCH 1/3] Adding support for storageclass --- src/app/mod.rs | 12 +++++- src/app/storageclass.rs | 66 ++++++++++++++++++++++++++++++ src/handlers/mod.rs | 16 ++++++++ src/network/kube_api.rs | 10 +++++ src/network/mod.rs | 4 ++ src/ui/resource_tabs.rs | 77 +++++++++++++++++++++++++++++++++++ test_data/storageclass.yaml | 80 +++++++++++++++++++++++++++++++++++++ 7 files changed, 264 insertions(+), 1 deletion(-) create mode 100644 src/app/storageclass.rs create mode 100644 test_data/storageclass.yaml diff --git a/src/app/mod.rs b/src/app/mod.rs index d03aae67..badc42e7 100644 --- a/src/app/mod.rs +++ b/src/app/mod.rs @@ -14,9 +14,11 @@ pub(crate) mod replicasets; pub(crate) mod replication_controllers; pub(crate) mod secrets; pub(crate) mod statefulsets; +pub(crate) mod storageclass; pub(crate) mod svcs; mod utils; +use crate::app::storageclass::KubeStorageClass; use anyhow::anyhow; use kube::config::Kubeconfig; use kubectl_view_allocations::{GroupBy, QtyByQualifier}; @@ -69,6 +71,7 @@ pub enum ActiveBlock { CronJobs, Secrets, RplCtrl, + StorageClasses, More, } @@ -123,6 +126,7 @@ pub struct Data { pub logs: LogsState, pub describe_out: ScrollableTxt, pub metrics: StatefulTable<(Vec, Option)>, + pub storageclasses: StatefulTable, } /// selected data items @@ -195,6 +199,7 @@ impl Default for Data { logs: LogsState::new(String::default()), describe_out: ScrollableTxt::new(), metrics: StatefulTable::new(), + storageclasses: StatefulTable::new(), } } } @@ -318,7 +323,7 @@ impl Default for App { ("Replication Controllers".into(), ActiveBlock::RplCtrl), // ("Persistent Volume Claims".into(), ActiveBlock::RplCtrl), // ("Persistent Volumes".into(), ActiveBlock::RplCtrl), - // ("Storage Classes".into(), ActiveBlock::RplCtrl), + ("Storage Classes".into(), ActiveBlock::StorageClasses), // ("Roles".into(), ActiveBlock::RplCtrl), // ("Role Bindings".into(), ActiveBlock::RplCtrl), // ("Cluster Roles".into(), ActiveBlock::RplCtrl), @@ -515,6 +520,7 @@ impl App { self.dispatch(IoEvent::GetCronJobs).await; self.dispatch(IoEvent::GetSecrets).await; self.dispatch(IoEvent::GetReplicationControllers).await; + self.dispatch(IoEvent::GetStorageClasses).await; self.dispatch(IoEvent::GetMetrics).await; } @@ -553,6 +559,9 @@ impl App { ActiveBlock::RplCtrl => { self.dispatch(IoEvent::GetReplicationControllers).await; } + ActiveBlock::StorageClasses => { + self.dispatch(IoEvent::GetStorageClasses).await; + } ActiveBlock::Logs => { if !self.is_streaming { // do not tail to avoid duplicates @@ -709,6 +718,7 @@ mod tests { sync_io_rx.recv().await.unwrap(), IoEvent::GetReplicationControllers ); + assert_eq!(sync_io_rx.recv().await.unwrap(), IoEvent::GetStorageClasses); assert_eq!(sync_io_rx.recv().await.unwrap(), IoEvent::GetMetrics); assert_eq!(sync_io_rx.recv().await.unwrap(), IoEvent::GetNamespaces); assert_eq!(sync_io_rx.recv().await.unwrap(), IoEvent::GetNodes); diff --git a/src/app/storageclass.rs b/src/app/storageclass.rs new file mode 100644 index 00000000..34d0d868 --- /dev/null +++ b/src/app/storageclass.rs @@ -0,0 +1,66 @@ +use crate::app::models::KubeResource; +use crate::app::utils; +use k8s_openapi::api::storage::v1::StorageClass; +use k8s_openapi::chrono::Utc; + +#[derive(Clone, Debug, PartialEq)] +pub struct KubeStorageClass { + pub name: String, + pub provisioner: String, + pub reclaim_policy: String, + pub volume_binding_mode: String, + pub allow_volume_expansion: bool, + pub age: String, + k8s_obj: StorageClass, +} + +impl KubeResource for KubeStorageClass { + fn from_api(storage_class: &StorageClass) -> Self { + KubeStorageClass { + name: storage_class.metadata.name.clone().unwrap_or_default(), + provisioner: storage_class.provisioner.clone(), + reclaim_policy: storage_class.reclaim_policy.clone().unwrap_or_default(), + volume_binding_mode: storage_class + .volume_binding_mode + .clone() + .unwrap_or_default(), + allow_volume_expansion: storage_class.allow_volume_expansion.unwrap_or_default(), + age: utils::to_age( + storage_class.metadata.creation_timestamp.as_ref(), + Utc::now(), + ), + k8s_obj: storage_class.to_owned(), + } + } + + fn get_k8s_obj(&self) -> &StorageClass { + &self.k8s_obj + } +} + +#[cfg(test)] +mod tests { + use crate::app::storageclass::KubeStorageClass; + use crate::app::test_utils::{convert_resource_from_file, get_time}; + use crate::app::utils; + use k8s_openapi::chrono::Utc; + + #[tokio::test] + async fn test_storageclass_from_api() { + let (storage_classes, storage_classes_list): (Vec, Vec<_>) = + convert_resource_from_file("storageclass"); + assert_eq!(storage_classes_list.len(), 4); + assert_eq!( + storage_classes[0], + KubeStorageClass { + name: "ebs-performance".into(), + provisioner: "kubernetes.io/aws-ebs".into(), + reclaim_policy: "Delete".into(), + volume_binding_mode: "Immediate".into(), + allow_volume_expansion: false, + age: utils::to_age(Some(&get_time("2021-12-14T11:08:59Z")), Utc::now()), + k8s_obj: storage_classes_list[0].clone(), + } + ); + } +} diff --git a/src/handlers/mod.rs b/src/handlers/mod.rs index fa742ab6..a0fd5636 100644 --- a/src/handlers/mod.rs +++ b/src/handlers/mod.rs @@ -416,6 +416,21 @@ async fn handle_route_events(key: Key, app: &mut App) { .await; } } + ActiveBlock::StorageClasses => { + if let Some(res) = handle_block_action(key, &mut app.data.storageclasses) { + let _ok = handle_describe_or_yaml_action( + key, + app, + &res, + IoCmdEvent::GetDescribe { + kind: "storageclass".to_owned(), + value: res.name.to_owned(), + ns: None, + }, + ) + .await; + } + } ActiveBlock::Contexts | ActiveBlock::Utilization | ActiveBlock::Help => { /* Do nothing */ } } } @@ -479,6 +494,7 @@ async fn handle_block_scroll(app: &mut App, up: bool, is_mouse: bool, page: bool ActiveBlock::RplCtrl => app.data.rpl_ctrls.handle_scroll(up, page), ActiveBlock::Contexts => app.data.contexts.handle_scroll(up, page), ActiveBlock::Utilization => app.data.metrics.handle_scroll(up, page), + ActiveBlock::StorageClasses => app.data.storageclasses.handle_scroll(up, page), ActiveBlock::Help => app.help_docs.handle_scroll(up, page), ActiveBlock::More => app.more_resources_menu.handle_scroll(up, page), ActiveBlock::Logs => { diff --git a/src/network/kube_api.rs b/src/network/kube_api.rs index 86631617..cc6f62da 100644 --- a/src/network/kube_api.rs +++ b/src/network/kube_api.rs @@ -30,6 +30,7 @@ use crate::app::{ replication_controllers::KubeReplicationController, secrets::KubeSecret, statefulsets::KubeStatefulSet, + storageclass::KubeStorageClass, svcs::KubeSvc, }; @@ -332,4 +333,13 @@ impl<'a> Network<'a> { None => Api::all(self.client.clone()), } } + + pub async fn get_storage_classes(&self) { + let items: Vec = self + .get_namespaced_resources(|it| KubeStorageClass::from_api(it)) + .await; + + let mut app = self.app.lock().await; + app.data.storageclasses.set_items(items); + } } diff --git a/src/network/mod.rs b/src/network/mod.rs index 82a361f9..0489f76c 100644 --- a/src/network/mod.rs +++ b/src/network/mod.rs @@ -27,6 +27,7 @@ pub enum IoEvent { GetCronJobs, GetSecrets, GetReplicationControllers, + GetStorageClasses, GetMetrics, RefreshClient, } @@ -154,6 +155,9 @@ impl<'a> Network<'a> { IoEvent::GetMetrics => { self.get_utilizations().await; } + IoEvent::GetStorageClasses => { + self.get_storage_classes().await; + } }; let mut app = self.app.lock().await; diff --git a/src/ui/resource_tabs.rs b/src/ui/resource_tabs.rs index 15b463b0..39de7143 100644 --- a/src/ui/resource_tabs.rs +++ b/src/ui/resource_tabs.rs @@ -34,6 +34,7 @@ static SECRETS_TITLE: &str = "Secrets"; static RPL_CTRL_TITLE: &str = "ReplicationControllers"; static DESCRIBE_ACTIVE: &str = "-> Describe "; static YAML_ACTIVE: &str = "-> YAML "; +static STORAGE_CLASSES_LABEL: &str = "Storage Classes"; pub fn draw_resource_tabs_block(f: &mut Frame<'_, B>, app: &mut App, area: Rect) { let chunks = @@ -80,6 +81,7 @@ fn draw_more(block: ActiveBlock, f: &mut Frame<'_, B>, app: &mut App ActiveBlock::CronJobs => draw_cronjobs_tab(block, f, app, area), ActiveBlock::Secrets => draw_secrets_tab(block, f, app, area), ActiveBlock::RplCtrl => draw_replication_controllers_tab(block, f, app, area), + ActiveBlock::StorageClasses => draw_storage_classes_tab(block, f, app, area), ActiveBlock::Describe | ActiveBlock::Yaml => { let mut prev_route = app.get_prev_route(); if prev_route.active_block == block { @@ -89,6 +91,7 @@ fn draw_more(block: ActiveBlock, f: &mut Frame<'_, B>, app: &mut App ActiveBlock::CronJobs => draw_cronjobs_tab(block, f, app, area), ActiveBlock::Secrets => draw_secrets_tab(block, f, app, area), ActiveBlock::RplCtrl => draw_replication_controllers_tab(block, f, app, area), + ActiveBlock::StorageClasses => draw_storage_classes_tab(block, f, app, area), _ => { /* do nothing */ } } } @@ -939,6 +942,68 @@ fn draw_replication_controllers_block(f: &mut Frame<'_, B>, app: &mu ); } +fn draw_storage_classes_tab( + block: ActiveBlock, + f: &mut Frame<'_, B>, + app: &mut App, + area: Rect, +) { + draw_resource_tab!( + STORAGE_CLASSES_LABEL, + block, + f, + app, + area, + draw_storage_classes_tab, + draw_storage_classes_block, + app.data.secrets + ); +} + +fn draw_storage_classes_block(f: &mut Frame<'_, B>, app: &mut App, area: Rect) { + let title = + get_cluster_wide_resource_title(STORAGE_CLASSES_LABEL, app.data.storageclasses.items.len()); + + draw_resource_block( + f, + area, + ResourceTableProps { + title, + inline_help: DESCRIBE_YAML_AND_ESC_HINT.into(), + resource: &mut app.data.storageclasses, + table_headers: vec![ + "Name", + "Provisioner", + "Reclaim Policy", + "Volume binding mode", + "Allow volume expansion", + "Age", + ], + column_widths: vec![ + Constraint::Percentage(10), + Constraint::Percentage(20), + Constraint::Percentage(10), + Constraint::Percentage(20), + Constraint::Percentage(20), + Constraint::Percentage(10), + ], + }, + |c| { + Row::new(vec![ + Cell::from(c.name.to_owned()), + Cell::from(c.provisioner.to_owned()), + Cell::from(c.reclaim_policy.to_owned()), + Cell::from(c.volume_binding_mode.to_owned()), + Cell::from(c.allow_volume_expansion.to_string()), + Cell::from(c.age.to_owned()), + ]) + .style(style_primary(app.light_theme)) + }, + app.light_theme, + app.is_loading, + ); +} + /// common for all resources fn draw_describe_block( f: &mut Frame<'_, B>, @@ -1037,6 +1102,10 @@ fn get_node_title>(app: &App, suffix: S) -> String { ) } +fn get_cluster_wide_resource_title>(title: S, items_len: usize) -> String { + format!("{} [{}]", title.as_ref(), items_len,) +} + fn get_resource_title>(app: &App, title: S, suffix: S, items_len: usize) -> String { format!( " {} {}", @@ -1370,4 +1439,12 @@ mod tests { fn test_title_with_ns() { assert_eq!(title_with_ns("Title", "hello", 3), "Title (ns: hello) [3]"); } + + #[test] + fn test_get_cluster_wide_resource_title() { + assert_eq!( + get_cluster_wide_resource_title("Cluster Resource", 3), + "Cluster Resource [3]" + ); + } } diff --git a/test_data/storageclass.yaml b/test_data/storageclass.yaml new file mode 100644 index 00000000..1a6a4e36 --- /dev/null +++ b/test_data/storageclass.yaml @@ -0,0 +1,80 @@ +apiVersion: v1 +items: +- apiVersion: storage.k8s.io/v1 + kind: StorageClass + metadata: + annotations: + meta.helm.sh/release-name: prometheus + meta.helm.sh/release-namespace: monitoring + creationTimestamp: "2021-12-14T11:08:59Z" + labels: + app.kubernetes.io/managed-by: Helm + helm.toolkit.fluxcd.io/name: prometheus + helm.toolkit.fluxcd.io/namespace: flux-system + name: ebs-performance + resourceVersion: "98487651" + uid: 4c55b509-35f6-4539-91f3-5efc04502287 + parameters: + iopsPerGB: "30" + type: io1 + provisioner: kubernetes.io/aws-ebs + reclaimPolicy: Delete + volumeBindingMode: Immediate +- allowVolumeExpansion: true + apiVersion: storage.k8s.io/v1 + kind: StorageClass + metadata: + annotations: + meta.helm.sh/release-name: prometheus + meta.helm.sh/release-namespace: monitoring + creationTimestamp: "2021-12-14T11:08:59Z" + labels: + app.kubernetes.io/managed-by: Helm + helm.toolkit.fluxcd.io/name: prometheus + helm.toolkit.fluxcd.io/namespace: flux-system + name: ebs-standard + resourceVersion: "98487650" + uid: 38ba70fb-25a8-4d9f-a1d3-2407de9e9128 + parameters: + type: gp2 + provisioner: kubernetes.io/aws-ebs + reclaimPolicy: Delete + volumeBindingMode: Immediate +- apiVersion: storage.k8s.io/v1 + kind: StorageClass + metadata: + annotations: + meta.helm.sh/release-name: prometheus + meta.helm.sh/release-namespace: monitoring + creationTimestamp: "2021-12-14T11:08:59Z" + labels: + app.kubernetes.io/managed-by: Helm + helm.toolkit.fluxcd.io/name: prometheus + helm.toolkit.fluxcd.io/namespace: flux-system + name: efs-sc + resourceVersion: "98487652" + uid: d60d2b3f-6e91-4fa1-add0-9383c0a8c6ea + provisioner: efs.csi.aws.com + reclaimPolicy: Delete + volumeBindingMode: Immediate +- apiVersion: storage.k8s.io/v1 + kind: StorageClass + metadata: + annotations: + kubectl.kubernetes.io/last-applied-configuration: | + {"apiVersion":"storage.k8s.io/v1","kind":"StorageClass","metadata":{"annotations":{"storageclass.kubernetes.io/is-default-class":"true"},"name":"gp2"},"parameters":{"fsType":"ext4","type":"gp2"},"provisioner":"kubernetes.io/aws-ebs","volumeBindingMode":"WaitForFirstConsumer"} + storageclass.kubernetes.io/is-default-class: "true" + creationTimestamp: "2021-12-14T11:04:25Z" + name: gp2 + resourceVersion: "183" + uid: 330bc0b5-b40c-4327-82ab-ca6f53b553cc + parameters: + fsType: ext4 + type: gp2 + provisioner: kubernetes.io/aws-ebs + reclaimPolicy: Delete + volumeBindingMode: WaitForFirstConsumer +kind: List +metadata: + resourceVersion: "" + selfLink: "" From 6152c2d61b6cb76bb068bb8f2e5395081bf4e056 Mon Sep 17 00:00:00 2001 From: Shinu Suresh Date: Wed, 27 Jul 2022 16:56:43 +0100 Subject: [PATCH 2/3] Responding to review comments --- src/app/mod.rs | 4 ++-- src/handlers/mod.rs | 2 +- src/network/kube_api.rs | 16 +++++++--------- src/ui/resource_tabs.rs | 4 ++-- 4 files changed, 12 insertions(+), 14 deletions(-) diff --git a/src/app/mod.rs b/src/app/mod.rs index badc42e7..124499c6 100644 --- a/src/app/mod.rs +++ b/src/app/mod.rs @@ -123,10 +123,10 @@ pub struct Data { pub cronjobs: StatefulTable, pub secrets: StatefulTable, pub rpl_ctrls: StatefulTable, + pub storageclasses: StatefulTable, pub logs: LogsState, pub describe_out: ScrollableTxt, pub metrics: StatefulTable<(Vec, Option)>, - pub storageclasses: StatefulTable, } /// selected data items @@ -190,6 +190,7 @@ impl Default for Data { cronjobs: StatefulTable::new(), secrets: StatefulTable::new(), rpl_ctrls: StatefulTable::new(), + storageclasses: StatefulTable::new(), selected: Selected { ns: None, pod: None, @@ -199,7 +200,6 @@ impl Default for Data { logs: LogsState::new(String::default()), describe_out: ScrollableTxt::new(), metrics: StatefulTable::new(), - storageclasses: StatefulTable::new(), } } } diff --git a/src/handlers/mod.rs b/src/handlers/mod.rs index a0fd5636..6e8f4e43 100644 --- a/src/handlers/mod.rs +++ b/src/handlers/mod.rs @@ -492,9 +492,9 @@ async fn handle_block_scroll(app: &mut App, up: bool, is_mouse: bool, page: bool ActiveBlock::CronJobs => app.data.cronjobs.handle_scroll(up, page), ActiveBlock::Secrets => app.data.secrets.handle_scroll(up, page), ActiveBlock::RplCtrl => app.data.rpl_ctrls.handle_scroll(up, page), + ActiveBlock::StorageClasses => app.data.storageclasses.handle_scroll(up, page), ActiveBlock::Contexts => app.data.contexts.handle_scroll(up, page), ActiveBlock::Utilization => app.data.metrics.handle_scroll(up, page), - ActiveBlock::StorageClasses => app.data.storageclasses.handle_scroll(up, page), ActiveBlock::Help => app.help_docs.handle_scroll(up, page), ActiveBlock::More => app.more_resources_menu.handle_scroll(up, page), ActiveBlock::Logs => { diff --git a/src/network/kube_api.rs b/src/network/kube_api.rs index cc6f62da..6655afad 100644 --- a/src/network/kube_api.rs +++ b/src/network/kube_api.rs @@ -298,7 +298,14 @@ impl<'a> Network<'a> { let mut app = self.app.lock().await; app.data.daemon_sets.set_items(items); } + pub async fn get_storage_classes(&self) { + let items: Vec = self + .get_namespaced_resources(|it| KubeStorageClass::from_api(it)) + .await; + let mut app = self.app.lock().await; + app.data.storageclasses.set_items(items); + } /// calls the kubernetes API to list the given resource for either selected namespace or all namespaces async fn get_namespaced_resources(&self, map_fn: F) -> Vec where @@ -333,13 +340,4 @@ impl<'a> Network<'a> { None => Api::all(self.client.clone()), } } - - pub async fn get_storage_classes(&self) { - let items: Vec = self - .get_namespaced_resources(|it| KubeStorageClass::from_api(it)) - .await; - - let mut app = self.app.lock().await; - app.data.storageclasses.set_items(items); - } } diff --git a/src/ui/resource_tabs.rs b/src/ui/resource_tabs.rs index 39de7143..040793b8 100644 --- a/src/ui/resource_tabs.rs +++ b/src/ui/resource_tabs.rs @@ -975,8 +975,8 @@ fn draw_storage_classes_block(f: &mut Frame<'_, B>, app: &mut App, a "Name", "Provisioner", "Reclaim Policy", - "Volume binding mode", - "Allow volume expansion", + "Volume Binding Mode", + "Allow Volume Expansion", "Age", ], column_widths: vec![ From d829103adb6cfceca378c0d89989ef915701c0a9 Mon Sep 17 00:00:00 2001 From: Deepu K Sasidharan Date: Fri, 29 Jul 2022 15:16:51 +0200 Subject: [PATCH 3/3] Update src/ui/resource_tabs.rs --- src/ui/resource_tabs.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ui/resource_tabs.rs b/src/ui/resource_tabs.rs index 040793b8..4a89be40 100644 --- a/src/ui/resource_tabs.rs +++ b/src/ui/resource_tabs.rs @@ -34,7 +34,7 @@ static SECRETS_TITLE: &str = "Secrets"; static RPL_CTRL_TITLE: &str = "ReplicationControllers"; static DESCRIBE_ACTIVE: &str = "-> Describe "; static YAML_ACTIVE: &str = "-> YAML "; -static STORAGE_CLASSES_LABEL: &str = "Storage Classes"; +static STORAGE_CLASSES_LABEL: &str = "StorageClasses"; pub fn draw_resource_tabs_block(f: &mut Frame<'_, B>, app: &mut App, area: Rect) { let chunks =