From fa6d5274d9ab2ede7fd41fc9002be4e73cf217c0 Mon Sep 17 00:00:00 2001 From: kohashimoto Date: Wed, 3 Jan 2024 00:26:25 +0900 Subject: [PATCH 1/3] feat(ui/window): add activate_tab_by_id method --- src/ui/window.rs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/ui/window.rs b/src/ui/window.rs index a3253e63..cd2c2e1b 100644 --- a/src/ui/window.rs +++ b/src/ui/window.rs @@ -276,6 +276,12 @@ impl<'a> Window<'a> { } } + pub fn activate_tab_by_id(&mut self, id: &str) { + if let Some(index) = self.tabs.iter().position(|tab| tab.id() == id) { + self.active_tab_index = index; + } + } + pub fn activate_next_tab(&mut self) { self.active_tab_index = (self.active_tab_index + 1) % self.tabs.len(); } From 6d8d7d9c2a46b9956892e6f40baab6f5a4ea77ec Mon Sep 17 00:00:00 2001 From: kohashimoto Date: Wed, 3 Jan 2024 00:27:09 +0900 Subject: [PATCH 2/3] feat(ui/table): add action callback --- src/ui/widget/table.rs | 35 ++++++++++++++++++++++++++++------- 1 file changed, 28 insertions(+), 7 deletions(-) diff --git a/src/ui/widget/table.rs b/src/ui/widget/table.rs index f68a078f..5e980da8 100644 --- a/src/ui/widget/table.rs +++ b/src/ui/widget/table.rs @@ -15,6 +15,7 @@ use filter_form::FilterForm; use item::InnerItem; use crate::{ + event::UserEvent, logger, ui::{ event::{Callback, EventResult}, @@ -34,7 +35,8 @@ const COLUMN_SPACING: u16 = 3; const HIGHLIGHT_SYMBOL: &str = " "; const ROW_START_INDEX: usize = 2; -type InnerCallback = Rc EventResult>; +type OnSelectCallback = Rc EventResult>; +type ActionCallback = Rc EventResult>; type RenderBlockInjection = Rc WidgetConfig>; type RenderHighlightInjection = Rc) -> Style>; @@ -49,7 +51,9 @@ pub struct TableBuilder { state: TableState, filtered_key: String, #[derivative(Debug = "ignore")] - on_select: Option, + on_select: Option, + #[derivative(Debug = "ignore")] + actions: Vec<(UserEvent, ActionCallback)>, #[derivative(Debug = "ignore")] block_injection: Option, #[derivative(Debug = "ignore")] @@ -93,6 +97,14 @@ impl TableBuilder { self } + pub fn action>(mut self, ev: E, cb: F) -> Self + where + F: Fn(&mut Window) -> EventResult + 'static, + { + self.actions.push((ev.into(), Rc::new(cb))); + self + } + pub fn block_injection(mut self, block_injection: F) -> Self where F: Fn(&Table) -> WidgetConfig + 'static, @@ -119,6 +131,7 @@ impl TableBuilder { id: self.id, widget_config: self.widget_config, on_select: self.on_select, + actions: self.actions, state: self.state, show_status: self.show_status, block_injection: self.block_injection, @@ -198,7 +211,9 @@ pub struct Table<'a> { filtered_key: String, mode: Mode, #[derivative(Debug = "ignore")] - on_select: Option, + on_select: Option, + #[derivative(Debug = "ignore")] + actions: Vec<(UserEvent, ActionCallback)>, #[derivative(Debug = "ignore")] block_injection: Option, #[derivative(Debug = "ignore")] @@ -522,11 +537,11 @@ impl WidgetTrait for Table<'_> { return EventResult::Callback(self.on_select_callback()); } - KeyCode::Char(_) => { - return EventResult::Ignore; - } - _ => { + if let Some(cb) = self.match_action(UserEvent::Key(ev)) { + return EventResult::Callback(Some(Callback::from(cb))); + } + return EventResult::Ignore; } }, @@ -600,6 +615,12 @@ impl<'a> Table<'a> { .selected() .and_then(|index| self.items().get(index).map(|item| Rc::new(item.clone()))) } + + fn match_action(&self, ev: UserEvent) -> Option { + self.actions + .iter() + .find_map(|(cb_ev, cb)| if *cb_ev == ev { Some(cb.clone()) } else { None }) + } } impl<'a> Table<'a> { From 934e6e096ead25bff02c2de7deac04bf81e9d46a Mon Sep 17 00:00:00 2001 From: kohashimoto Date: Wed, 3 Jan 2024 00:28:38 +0900 Subject: [PATCH 3/3] feat: Support opening YAML popup from resource list --- src/action.rs | 12 +- src/event/kubernetes.rs | 35 +- src/event/kubernetes/yaml.rs | 445 +----------------- src/event/kubernetes/yaml/direct.rs | 162 +++++++ src/event/kubernetes/yaml/select.rs | 11 + src/event/kubernetes/yaml/select/resources.rs | 104 ++++ .../select/resources/multiple_namespaces.rs | 68 +++ .../yaml/select/resources/not_namespace.rs | 44 ++ .../yaml/select/resources/single_namespace.rs | 56 +++ src/event/kubernetes/yaml/select/worker.rs | 140 ++++++ src/window.rs | 91 +++- src/window/yaml.rs | 9 +- src/window/yaml_popup.rs | 45 ++ 13 files changed, 770 insertions(+), 452 deletions(-) create mode 100644 src/event/kubernetes/yaml/direct.rs create mode 100644 src/event/kubernetes/yaml/select.rs create mode 100644 src/event/kubernetes/yaml/select/resources.rs create mode 100644 src/event/kubernetes/yaml/select/resources/multiple_namespaces.rs create mode 100644 src/event/kubernetes/yaml/select/resources/not_namespace.rs create mode 100644 src/event/kubernetes/yaml/select/resources/single_namespace.rs create mode 100644 src/event/kubernetes/yaml/select/worker.rs create mode 100644 src/window/yaml_popup.rs diff --git a/src/action.rs b/src/action.rs index a3265a64..ca7d427e 100644 --- a/src/action.rs +++ b/src/action.rs @@ -62,6 +62,8 @@ pub mod view_id { generate_id!(popup_yaml_kind); generate_id!(popup_yaml_return); + generate_id!(popup_yaml); + generate_id!(popup_help); } @@ -440,9 +442,17 @@ pub fn update_contents( widget.update_widget_item(Item::Array(error_lines!(e))); } }, - Yaml(res) => { + SelectedYaml(res) => { update_widget_item_for_vec(window, view_id::tab_yaml_widget_yaml, res); } + DirectedYaml { kind, name, yaml } => { + let widget = window + .find_widget_mut(view_id::popup_yaml) + .widget_config_mut(); + *(widget.append_title_mut()) = Some(format!(" : {}/{}", kind, name).into()); + + update_widget_item_for_vec(window, view_id::popup_yaml, yaml); + } } } diff --git a/src/event/kubernetes.rs b/src/event/kubernetes.rs index 68235e15..659ec28b 100644 --- a/src/event/kubernetes.rs +++ b/src/event/kubernetes.rs @@ -43,8 +43,8 @@ use self::{ pod::{LogMessage, LogWorker}, worker::{AbortWorker, PollWorker, Worker}, yaml::{ - fetch_resource_list::FetchResourceList, - worker::{YamlWorker, YamlWorkerRequest}, + direct::DirectedYamlWorker, + select::{resources::FetchResourceList, worker::SelectedYamlWorker}, YamlMessage, YamlRequest, YamlResponse, }, }; @@ -481,23 +481,13 @@ impl Worker for MainWorker { tx.send(YamlResponse::Resource(fetched_data).into()) .expect("Failed to send YamlResponse::Resource"); } - Yaml { - kind, - name, - namespace, - } => { + SelectedYaml(req) => { if let Some(handler) = yaml_handler { handler.abort(); } - let req = YamlWorkerRequest { - kind, - name, - namespace, - }; - yaml_handler = Some( - YamlWorker::new( + SelectedYamlWorker::new( is_terminated.clone(), tx, kube_client.clone(), @@ -508,6 +498,23 @@ impl Worker for MainWorker { ); task::yield_now().await; } + + DirectedYaml(req) => { + if let Some(handler) = yaml_handler { + handler.abort(); + } + + yaml_handler = Some( + DirectedYamlWorker::new( + is_terminated.clone(), + tx, + kube_client.clone(), + req, + ) + .spawn(), + ); + task::yield_now().await; + } } } diff --git a/src/event/kubernetes/yaml.rs b/src/event/kubernetes/yaml.rs index f1d5eef2..3eca976b 100644 --- a/src/event/kubernetes/yaml.rs +++ b/src/event/kubernetes/yaml.rs @@ -1,19 +1,11 @@ -use std::sync::{atomic::AtomicBool, Arc}; +pub mod direct; +pub mod select; -use crossbeam::channel::Sender; +use self::{direct::DirectedYaml, select::SelectedYaml}; -use k8s_openapi::apimachinery::pkg::apis::meta::v1::ObjectMeta; -use serde::Deserialize; +use super::{api_resources::ApiResource, Kube}; -use super::{ - api_resources::{ApiResource, ApiResources, SharedApiResources}, - client::KubeClientRequest, - Kube, -}; -use crate::{ - error::{Error, Result}, - event::Event, -}; +use crate::{error::Result, event::Event}; #[derive(Debug, Clone)] pub struct YamlResourceListItem { @@ -38,11 +30,8 @@ impl YamlResourceList { pub enum YamlRequest { APIs, Resource(ApiResource), - Yaml { - kind: ApiResource, - name: String, - namespace: String, - }, + SelectedYaml(SelectedYaml), + DirectedYaml(DirectedYaml), } impl From for Event { @@ -55,7 +44,12 @@ impl From for Event { pub enum YamlResponse { APIs(Result>), Resource(Result), - Yaml(Result>), + SelectedYaml(Result>), + DirectedYaml { + kind: String, + name: String, + yaml: Result>, + }, } impl From for Event { @@ -81,416 +75,3 @@ impl From for Event { Self::Kube(m.into()) } } - -pub mod fetch_resource_list { - use crate::event::kubernetes::api_resources::ApiResource; - use crate::event::kubernetes::yaml::fetch_resource_list::not_namespaced::FetchResourceListNotNamespaced; - use crate::event::kubernetes::TargetNamespaces; - - use self::multiple_namespace::FetchResourceListMultipleNamespaces; - - use self::single_namespace::FetchResourceListSingleNamespace; - - use super::*; - - #[derive(Default, Debug, Clone, Deserialize)] - #[serde(rename_all = "camelCase")] - struct List { - items: Vec, - } - - #[derive(Default, Debug, Clone, Deserialize)] - #[serde(rename_all = "camelCase")] - struct Item { - metadata: ObjectMeta, - } - - mod not_namespaced { - use anyhow::Result; - - use crate::{ - event::kubernetes::{ - api_resources::ApiResource, client::KubeClientRequest, yaml::YamlResourceListItem, - }, - logger, - }; - - use super::List; - - pub(super) struct FetchResourceListNotNamespaced<'a, C: KubeClientRequest> { - client: &'a C, - api: &'a ApiResource, - kind: &'a str, - } - - impl<'a, C: KubeClientRequest> FetchResourceListNotNamespaced<'a, C> { - pub(super) fn new(client: &'a C, api: &'a ApiResource, kind: &'a str) -> Self { - Self { client, api, kind } - } - - pub(super) async fn fetch(&self) -> Result> { - let path = format!("{}/{}", self.api.group_version_url(), self.kind); - logger!(info, "Fetching resource [{}]", path); - - let res: List = self.client.request(&path).await?; - - logger!(info, "Fetched resource - {:?}", res); - - Ok(res - .items - .into_iter() - .filter_map(|item| { - item.metadata.name.map(|name| YamlResourceListItem { - namespace: "".to_string(), - name: name.to_string(), - kind: self.api.clone(), - value: name, - }) - }) - .collect()) - } - } - } - - mod single_namespace { - use anyhow::Result; - - use crate::{ - event::kubernetes::{ - api_resources::ApiResource, client::KubeClientRequest, yaml::YamlResourceListItem, - }, - logger, - }; - - use super::List; - - pub(super) struct FetchResourceListSingleNamespace<'a, C: KubeClientRequest> { - client: &'a C, - ns: &'a str, - api: &'a ApiResource, - kind: &'a str, - } - - impl<'a, C: KubeClientRequest> FetchResourceListSingleNamespace<'a, C> { - pub(super) fn new( - client: &'a C, - ns: &'a str, - api: &'a ApiResource, - kind: &'a str, - ) -> Self { - Self { - client, - ns, - api, - kind, - } - } - - pub(super) async fn fetch(&self) -> Result> { - let path = format!( - "{}/namespaces/{}/{}", - self.api.group_version_url(), - self.ns, - self.kind - ); - - logger!(info, "Fetching resource [{}]", path); - - let res: List = self.client.request(&path).await?; - - logger!(info, "Fetched resource - {:?}", res); - - Ok(res - .items - .into_iter() - .filter_map(|item| { - item.metadata.name.map(|name| YamlResourceListItem { - namespace: self.ns.to_string(), - name: name.to_string(), - kind: self.api.clone(), - value: name, - }) - }) - .collect()) - } - } - } - - mod multiple_namespace { - - use anyhow::Result; - use futures::future::try_join_all; - use unicode_segmentation::UnicodeSegmentation; - - use crate::event::kubernetes::{ - api_resources::ApiResource, client::KubeClientRequest, yaml::YamlResourceListItem, - }; - - use super::single_namespace::FetchResourceListSingleNamespace; - - pub(super) struct FetchResourceListMultipleNamespaces<'a, C: KubeClientRequest> { - client: &'a C, - namespaces: &'a [String], - api: &'a ApiResource, - kind: &'a str, - } - - impl<'a, C: KubeClientRequest> FetchResourceListMultipleNamespaces<'a, C> { - pub(super) fn new( - client: &'a C, - namespaces: &'a [String], - api: &'a ApiResource, - kind: &'a str, - ) -> Self { - Self { - client, - namespaces, - api, - kind, - } - } - - pub(super) async fn fetch(&self) -> Result> { - let jobs = try_join_all(self.namespaces.iter().map(|ns| async move { - FetchResourceListSingleNamespace::new(self.client, ns, self.api, self.kind) - .fetch() - .await - })) - .await?; - - let namespace_digit = self - .namespaces - .iter() - .map(|ns| ns.graphemes(true).count()) - .max() - .unwrap_or(0); - - let list = jobs - .into_iter() - .flat_map(|items| { - items - .into_iter() - .map(|mut item| { - item.value = format!( - "{:digit$} {}", - item.namespace, - item.name, - digit = namespace_digit - ); - item - }) - .collect::>() - }) - .collect(); - - Ok(list) - } - } - } - - pub struct FetchResourceList<'a, C: KubeClientRequest> { - client: &'a C, - req: ApiResource, - target_namespaces: &'a TargetNamespaces, - api_resources: &'a ApiResources, - } - - impl<'a, C: KubeClientRequest> FetchResourceList<'a, C> { - pub fn new( - client: &'a C, - req: ApiResource, - api_resources: &'a ApiResources, - target_namespaces: &'a TargetNamespaces, - ) -> Self { - Self { - client, - req, - api_resources, - target_namespaces, - } - } - - /// 選択されているリソースのリストを取得する - /// - /// ネームスペースが1つのとき OR namespaced が false のとき - /// リソース一覧を返す - /// - /// ネームスペースが2つ以上のとき - /// ネームスペースを頭につけたリソース一覧を返す - /// - pub async fn fetch(&self) -> Result { - let kind = &self.req; - - let api = self - .api_resources - .get(kind) - .ok_or_else(|| Error::Raw(format!("Can't get {} from API resource", kind)))?; - - let kind = &api.name(); - let list = if api.is_namespaced() { - if self.target_namespaces.len() == 1 { - FetchResourceListSingleNamespace::new( - self.client, - &self.target_namespaces[0], - api, - kind, - ) - .fetch() - .await? - } else { - FetchResourceListMultipleNamespaces::new( - self.client, - self.target_namespaces, - api, - kind, - ) - .fetch() - .await? - } - } else { - FetchResourceListNotNamespaced::new(self.client, api, kind) - .fetch() - .await? - }; - - Ok(YamlResourceList::new(list)) - } - } -} - -pub mod worker { - use serde_yaml::Value; - - use crate::{ - event::kubernetes::{api_resources::ApiResource, worker::AbortWorker}, - logger, - }; - - use super::*; - - #[derive(Debug, Clone)] - pub struct YamlWorkerRequest { - pub namespace: String, - pub kind: ApiResource, - pub name: String, - } - - #[derive(Debug, Clone)] - pub struct YamlWorker - where - C: KubeClientRequest, - { - is_terminated: Arc, - tx: Sender, - client: C, - req: YamlWorkerRequest, - shared_api_resources: SharedApiResources, - } - - impl YamlWorker { - pub fn new( - is_terminated: Arc, - tx: Sender, - client: C, - shared_api_resources: SharedApiResources, - req: YamlWorkerRequest, - ) -> Self { - Self { - is_terminated, - tx, - client, - req, - shared_api_resources, - } - } - } - - #[async_trait::async_trait] - impl AbortWorker for YamlWorker { - async fn run(&self) { - let mut interval = tokio::time::interval(std::time::Duration::from_secs(3)); - - let YamlWorkerRequest { - kind, - name, - namespace, - } = &self.req; - - while !self - .is_terminated - .load(std::sync::atomic::Ordering::Relaxed) - { - interval.tick().await; - - let api_resources = self.shared_api_resources.read().await; - - let fetched_data = fetch_resource_yaml( - &self.client, - &api_resources, - kind, - name.to_string(), - namespace.to_string(), - ) - .await; - - self.tx - .send(YamlResponse::Yaml(fetched_data).into()) - .expect("Failed to send YamlResponse::Yaml"); - } - } - } - - /// 選択されているリソースのyamlを取得する - pub async fn fetch_resource_yaml( - client: &C, - api_resources: &ApiResources, - kind: &ApiResource, - name: String, - ns: String, - ) -> Result> { - logger!( - info, - "Fetching resource target [kind={} ns={} name={}]", - kind, - ns, - name - ); - - let api = api_resources - .get(kind) - .ok_or_else(|| Error::Raw(format!("Can't get {} from API resource", kind)))?; - // json string data - let kind = api.name(); - let path = if api.is_namespaced() { - format!( - "{}/namespaces/{}/{}/{}", - api.group_version_url(), - ns, - kind, - name - ) - } else { - format!("{}/{}/{}", api.group_version_url(), kind, name) - }; - - logger!(info, "Fetching resource [{}]", path); - - let res = client.request_text(&path).await?; - - logger!(info, "Fetched resource - {}", res); - - // yaml dataに変換 - let mut yaml_data: serde_yaml::Value = serde_json::from_str(&res)?; - - if let Some(Value::Mapping(md)) = yaml_data.get_mut("metadata") { - md.remove("managedFields"); - } - - let yaml_string = serde_yaml::to_string(&yaml_data)? - .lines() - .map(ToString::to_string) - .collect(); - - Ok(yaml_string) - } -} diff --git a/src/event/kubernetes/yaml/direct.rs b/src/event/kubernetes/yaml/direct.rs new file mode 100644 index 00000000..d512ca30 --- /dev/null +++ b/src/event/kubernetes/yaml/direct.rs @@ -0,0 +1,162 @@ +use std::sync::{atomic::AtomicBool, Arc}; + +use anyhow::Result; +use crossbeam::channel::Sender; +use k8s_openapi::{ + api::{ + core::v1::{ConfigMap, Pod, Secret, Service}, + networking::v1::{Ingress, NetworkPolicy}, + }, + NamespaceResourceScope, +}; +use kube::{Api, Resource}; +use serde::{de::DeserializeOwned, Serialize}; + +use crate::{ + event::{ + kubernetes::{client::KubeClient, worker::AbortWorker, yaml::YamlResponse}, + Event, + }, + logger, +}; + +#[derive(Debug, Clone)] +pub struct DirectedYaml { + pub name: String, + pub namespace: String, + pub kind: DirectedYamlKind, +} + +#[derive(Debug, Clone)] +pub enum DirectedYamlKind { + Pod, + ConfigMap, + Secret, + Ingress, + Service, + NetworkPolicy, +} + +impl std::fmt::Display for DirectedYamlKind { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + DirectedYamlKind::Pod => write!(f, "pods"), + DirectedYamlKind::ConfigMap => write!(f, "configmaps"), + DirectedYamlKind::Secret => write!(f, "secrets"), + DirectedYamlKind::Ingress => write!(f, "ingresses"), + DirectedYamlKind::Service => write!(f, "services"), + DirectedYamlKind::NetworkPolicy => write!(f, "networkpolicies"), + } + } +} + +#[derive(Clone)] +pub struct DirectedYamlWorker { + is_terminated: Arc, + tx: Sender, + client: KubeClient, + req: DirectedYaml, +} + +impl DirectedYamlWorker { + pub fn new( + is_terminated: Arc, + tx: Sender, + client: KubeClient, + req: DirectedYaml, + ) -> Self { + Self { + is_terminated, + tx, + client, + req, + } + } +} + +#[async_trait::async_trait] +impl AbortWorker for DirectedYamlWorker { + async fn run(&self) { + let mut interval = tokio::time::interval(std::time::Duration::from_secs(3)); + + let DirectedYaml { + kind, + name, + namespace, + } = &self.req; + + while !self + .is_terminated + .load(std::sync::atomic::Ordering::Relaxed) + { + interval.tick().await; + + let yaml = match kind { + DirectedYamlKind::Pod => { + fetch_resource_yaml::(&self.client, name, namespace).await + } + DirectedYamlKind::ConfigMap => { + fetch_resource_yaml::(&self.client, name, namespace).await + } + DirectedYamlKind::Secret => { + fetch_resource_yaml::(&self.client, name, namespace).await + } + DirectedYamlKind::Ingress => { + fetch_resource_yaml::(&self.client, name, namespace).await + } + DirectedYamlKind::Service => { + fetch_resource_yaml::(&self.client, name, namespace).await + } + DirectedYamlKind::NetworkPolicy => { + fetch_resource_yaml::(&self.client, name, namespace).await + } + }; + + self.tx + .send( + YamlResponse::DirectedYaml { + yaml, + kind: kind.to_string(), + name: name.to_string(), + } + .into(), + ) + .expect("Failed to send YamlResponse::Yaml"); + } + } +} + +/// 選択されているリソースのyamlを取得する +pub async fn fetch_resource_yaml( + client: &KubeClient, + name: &str, + ns: &str, +) -> Result> +where + K: Resource + k8s_openapi::Resource, + ::DynamicType: Default, + K: DeserializeOwned + Clone + std::fmt::Debug, + K: Serialize, +{ + logger!( + info, + "Fetching resource target [kind={} ns={} name={}]", + K::KIND, + ns, + name + ); + + let api: Api = Api::namespaced(client.to_client(), ns); + + let mut data = api.get(name).await?; + + let metadata = data.meta_mut(); + metadata.managed_fields = None; + + let yaml_string = serde_yaml::to_string(&data)? + .lines() + .map(ToString::to_string) + .collect(); + + Ok(yaml_string) +} diff --git a/src/event/kubernetes/yaml/select.rs b/src/event/kubernetes/yaml/select.rs new file mode 100644 index 00000000..970c4e1e --- /dev/null +++ b/src/event/kubernetes/yaml/select.rs @@ -0,0 +1,11 @@ +pub mod resources; +pub mod worker; + +use crate::event::kubernetes::api_resources::ApiResource; + +#[derive(Debug, Clone)] +pub struct SelectedYaml { + pub kind: ApiResource, + pub name: String, + pub namespace: String, +} diff --git a/src/event/kubernetes/yaml/select/resources.rs b/src/event/kubernetes/yaml/select/resources.rs new file mode 100644 index 00000000..9a1f9f98 --- /dev/null +++ b/src/event/kubernetes/yaml/select/resources.rs @@ -0,0 +1,104 @@ +mod multiple_namespaces; +mod not_namespace; +mod single_namespace; + +use anyhow::Result; +use k8s_openapi::apimachinery::pkg::apis::meta::v1::ObjectMeta; +use serde::Deserialize; + +use crate::{ + error::Error, + event::kubernetes::{ + api_resources::{ApiResource, ApiResources}, + client::KubeClientRequest, + yaml::YamlResourceList, + TargetNamespaces, + }, +}; + +use self::{ + multiple_namespaces::FetchResourceListMultipleNamespaces, + not_namespace::FetchResourceListNotNamespaced, + single_namespace::FetchResourceListSingleNamespace, +}; + +#[derive(Default, Debug, Clone, Deserialize)] +#[serde(rename_all = "camelCase")] +struct List { + items: Vec, +} + +#[derive(Default, Debug, Clone, Deserialize)] +#[serde(rename_all = "camelCase")] +struct Item { + metadata: ObjectMeta, +} + +pub struct FetchResourceList<'a, C: KubeClientRequest> { + client: &'a C, + req: ApiResource, + target_namespaces: &'a TargetNamespaces, + api_resources: &'a ApiResources, +} + +impl<'a, C: KubeClientRequest> FetchResourceList<'a, C> { + pub fn new( + client: &'a C, + req: ApiResource, + api_resources: &'a ApiResources, + target_namespaces: &'a TargetNamespaces, + ) -> Self { + Self { + client, + req, + api_resources, + target_namespaces, + } + } + + /// 選択されているリソースのリストを取得する + /// + /// ネームスペースが1つのとき OR namespaced が false のとき + /// リソース一覧を返す + /// + /// ネームスペースが2つ以上のとき + /// ネームスペースを頭につけたリソース一覧を返す + /// + pub async fn fetch(&self) -> Result { + let kind = &self.req; + + let api = self + .api_resources + .get(kind) + .ok_or_else(|| Error::Raw(format!("Can't get {} from API resource", kind)))?; + + let kind = &api.name(); + let list = if api.is_namespaced() { + if self.target_namespaces.len() == 1 { + FetchResourceListSingleNamespace::new( + self.client, + &self.target_namespaces[0], + api, + kind, + ) + .fetch() + .await? + } else { + FetchResourceListMultipleNamespaces::new( + self.client, + self.target_namespaces, + api, + kind, + ) + .fetch() + .await? + } + } else { + FetchResourceListNotNamespaced::new(self.client, api, kind) + .fetch() + .await? + }; + + Ok(YamlResourceList::new(list)) + } +} diff --git a/src/event/kubernetes/yaml/select/resources/multiple_namespaces.rs b/src/event/kubernetes/yaml/select/resources/multiple_namespaces.rs new file mode 100644 index 00000000..81542e86 --- /dev/null +++ b/src/event/kubernetes/yaml/select/resources/multiple_namespaces.rs @@ -0,0 +1,68 @@ +use anyhow::Result; +use futures::future::try_join_all; +use unicode_segmentation::UnicodeSegmentation; + +use crate::event::kubernetes::{ + api_resources::ApiResource, client::KubeClientRequest, yaml::YamlResourceListItem, +}; + +use super::single_namespace::FetchResourceListSingleNamespace; + +pub(super) struct FetchResourceListMultipleNamespaces<'a, C: KubeClientRequest> { + client: &'a C, + namespaces: &'a [String], + api: &'a ApiResource, + kind: &'a str, +} + +impl<'a, C: KubeClientRequest> FetchResourceListMultipleNamespaces<'a, C> { + pub(super) fn new( + client: &'a C, + namespaces: &'a [String], + api: &'a ApiResource, + kind: &'a str, + ) -> Self { + Self { + client, + namespaces, + api, + kind, + } + } + + pub(super) async fn fetch(&self) -> Result> { + let jobs = try_join_all(self.namespaces.iter().map(|ns| async move { + FetchResourceListSingleNamespace::new(self.client, ns, self.api, self.kind) + .fetch() + .await + })) + .await?; + + let namespace_digit = self + .namespaces + .iter() + .map(|ns| ns.graphemes(true).count()) + .max() + .unwrap_or(0); + + let list = jobs + .into_iter() + .flat_map(|items| { + items + .into_iter() + .map(|mut item| { + item.value = format!( + "{:digit$} {}", + item.namespace, + item.name, + digit = namespace_digit + ); + item + }) + .collect::>() + }) + .collect(); + + Ok(list) + } +} diff --git a/src/event/kubernetes/yaml/select/resources/not_namespace.rs b/src/event/kubernetes/yaml/select/resources/not_namespace.rs new file mode 100644 index 00000000..ccb75033 --- /dev/null +++ b/src/event/kubernetes/yaml/select/resources/not_namespace.rs @@ -0,0 +1,44 @@ +use anyhow::Result; + +use crate::{ + event::kubernetes::{ + api_resources::ApiResource, client::KubeClientRequest, yaml::YamlResourceListItem, + }, + logger, +}; + +use super::List; + +pub(super) struct FetchResourceListNotNamespaced<'a, C: KubeClientRequest> { + client: &'a C, + api: &'a ApiResource, + kind: &'a str, +} + +impl<'a, C: KubeClientRequest> FetchResourceListNotNamespaced<'a, C> { + pub(super) fn new(client: &'a C, api: &'a ApiResource, kind: &'a str) -> Self { + Self { client, api, kind } + } + + pub(super) async fn fetch(&self) -> Result> { + let path = format!("{}/{}", self.api.group_version_url(), self.kind); + logger!(info, "Fetching resource [{}]", path); + + let res: List = self.client.request(&path).await?; + + logger!(info, "Fetched resource - {:?}", res); + + Ok(res + .items + .into_iter() + .filter_map(|item| { + item.metadata.name.map(|name| YamlResourceListItem { + namespace: "".to_string(), + name: name.to_string(), + kind: self.api.clone(), + value: name, + }) + }) + .collect()) + } +} diff --git a/src/event/kubernetes/yaml/select/resources/single_namespace.rs b/src/event/kubernetes/yaml/select/resources/single_namespace.rs new file mode 100644 index 00000000..8129f9f2 --- /dev/null +++ b/src/event/kubernetes/yaml/select/resources/single_namespace.rs @@ -0,0 +1,56 @@ +use anyhow::Result; + +use crate::{ + event::kubernetes::{ + api_resources::ApiResource, client::KubeClientRequest, yaml::YamlResourceListItem, + }, + logger, +}; + +use super::List; + +pub(super) struct FetchResourceListSingleNamespace<'a, C: KubeClientRequest> { + client: &'a C, + ns: &'a str, + api: &'a ApiResource, + kind: &'a str, +} + +impl<'a, C: KubeClientRequest> FetchResourceListSingleNamespace<'a, C> { + pub(super) fn new(client: &'a C, ns: &'a str, api: &'a ApiResource, kind: &'a str) -> Self { + Self { + client, + ns, + api, + kind, + } + } + + pub(super) async fn fetch(&self) -> Result> { + let path = format!( + "{}/namespaces/{}/{}", + self.api.group_version_url(), + self.ns, + self.kind + ); + + logger!(info, "Fetching resource [{}]", path); + + let res: List = self.client.request(&path).await?; + + logger!(info, "Fetched resource - {:?}", res); + + Ok(res + .items + .into_iter() + .filter_map(|item| { + item.metadata.name.map(|name| YamlResourceListItem { + namespace: self.ns.to_string(), + name: name.to_string(), + kind: self.api.clone(), + value: name, + }) + }) + .collect()) + } +} diff --git a/src/event/kubernetes/yaml/select/worker.rs b/src/event/kubernetes/yaml/select/worker.rs new file mode 100644 index 00000000..ebf44eb2 --- /dev/null +++ b/src/event/kubernetes/yaml/select/worker.rs @@ -0,0 +1,140 @@ +use std::sync::{atomic::AtomicBool, Arc}; + +use anyhow::Result; +use crossbeam::channel::Sender; +use serde_yaml::Value; + +use crate::{ + error::Error, + event::{ + kubernetes::{ + api_resources::{ApiResource, ApiResources, SharedApiResources}, + client::KubeClientRequest, + worker::AbortWorker, + yaml::YamlResponse, + }, + Event, + }, + logger, +}; + +use super::SelectedYaml; + +#[derive(Debug, Clone)] +pub struct SelectedYamlWorker +where + C: KubeClientRequest, +{ + is_terminated: Arc, + tx: Sender, + client: C, + req: SelectedYaml, + shared_api_resources: SharedApiResources, +} + +impl SelectedYamlWorker { + pub fn new( + is_terminated: Arc, + tx: Sender, + client: C, + shared_api_resources: SharedApiResources, + req: SelectedYaml, + ) -> Self { + Self { + is_terminated, + tx, + client, + req, + shared_api_resources, + } + } +} + +#[async_trait::async_trait] +impl AbortWorker for SelectedYamlWorker { + async fn run(&self) { + let mut interval = tokio::time::interval(std::time::Duration::from_secs(3)); + + let SelectedYaml { + kind, + name, + namespace, + } = &self.req; + + while !self + .is_terminated + .load(std::sync::atomic::Ordering::Relaxed) + { + interval.tick().await; + + let api_resources = self.shared_api_resources.read().await; + + let fetched_data = fetch_resource_yaml( + &self.client, + &api_resources, + kind, + name.to_string(), + namespace.to_string(), + ) + .await; + + self.tx + .send(YamlResponse::SelectedYaml(fetched_data).into()) + .expect("Failed to send YamlResponse::Yaml"); + } + } +} + +/// 選択されているリソースのyamlを取得する +pub async fn fetch_resource_yaml( + client: &C, + api_resources: &ApiResources, + kind: &ApiResource, + name: String, + ns: String, +) -> Result> { + logger!( + info, + "Fetching resource target [kind={} ns={} name={}]", + kind, + ns, + name + ); + + let api = api_resources + .get(kind) + .ok_or_else(|| Error::Raw(format!("Can't get {} from API resource", kind)))?; + // json string data + let kind = api.name(); + let path = if api.is_namespaced() { + format!( + "{}/namespaces/{}/{}/{}", + api.group_version_url(), + ns, + kind, + name + ) + } else { + format!("{}/{}/{}", api.group_version_url(), kind, name) + }; + + logger!(info, "Fetching resource [{}]", path); + + let res = client.request_text(&path).await?; + + logger!(info, "Fetched resource - {}", res); + + // yaml dataに変換 + let mut yaml_data: serde_yaml::Value = serde_json::from_str(&res)?; + + if let Some(Value::Mapping(md)) = yaml_data.get_mut("metadata") { + md.remove("managedFields"); + } + + let yaml_string = serde_yaml::to_string(&yaml_data)? + .lines() + .map(ToString::to_string) + .collect(); + + Ok(yaml_string) +} diff --git a/src/window.rs b/src/window.rs index e3b1888e..384adf09 100644 --- a/src/window.rs +++ b/src/window.rs @@ -6,6 +6,7 @@ mod list; mod network; mod pod; mod yaml; +mod yaml_popup; use std::{cell::RefCell, rc::Rc}; @@ -18,10 +19,22 @@ use crate::{ clipboard_wrapper::Clipboard, context::{Context, Namespace}, event::{ - kubernetes::{context_message::ContextRequest, namespace_message::NamespaceRequest}, + kubernetes::{ + context_message::ContextRequest, + namespace_message::NamespaceRequest, + yaml::{ + direct::{DirectedYaml, DirectedYamlKind}, + YamlRequest, + }, + }, Event, UserEvent, }, - ui::{event::EventResult, popup::Popup, Header, Tab, Window, WindowEvent}, + ui::{ + event::EventResult, + popup::Popup, + widget::{SelectedItem, WidgetTrait}, + Header, Tab, Window, WindowEvent, + }, }; use self::{ @@ -33,6 +46,7 @@ use self::{ network::{NetworkTab, NetworkTabBuilder}, pod::{PodTabBuilder, PodsTab}, yaml::{YamlTab, YamlTabBuilder}, + yaml_popup::{YamlPopup, YamlPopupBuilder}, }; pub struct WindowInit { @@ -104,7 +118,10 @@ impl WindowInit { EventResult::Nop }; + let open_yaml = open_yaml(self.tx.clone()); + let builder = builder.action('h', open_help).action('?', open_help); + let builder = builder.action('y', open_yaml); let builder = builder.action('q', fn_close).action(KeyCode::Esc, fn_close); @@ -170,6 +187,8 @@ impl WindowInit { content: popup_help, } = HelpPopup::new(); + let YamlPopup { popup: popup_yaml } = YamlPopupBuilder::new(&clipboard).build(); + // Init Window let tabs = vec![ tab_pods, @@ -190,8 +209,76 @@ impl WindowInit { Popup::new(popup_yaml_return), Popup::new(popup_help), Popup::new(popup_log_query_help), + Popup::new(popup_yaml), ]; (tabs, popups) } } + +fn open_yaml(tx: Sender) -> impl Fn(&mut Window) -> EventResult { + move |w: &mut Window| { + let widget = w.active_tab().active_widget(); + + match widget.id() { + view_id::tab_pod_widget_pod + | view_id::tab_config_widget_config + | view_id::tab_network_widget_network => {} + _ => { + return EventResult::Ignore; + } + } + + let Some(SelectedItem::TableRow { metadata, .. }) = widget.widget_item() else { + return EventResult::Ignore; + }; + + let Some(ref metadata) = metadata else { + return EventResult::Ignore; + }; + + let Some(ref namespace) = metadata.get("namespace") else { + return EventResult::Ignore; + }; + + let Some(ref name) = metadata.get("name") else { + return EventResult::Ignore; + }; + + let kind = match widget.id() { + view_id::tab_pod_widget_pod => DirectedYamlKind::Pod, + view_id::tab_config_widget_config => match metadata.get("kind").map(|v| v.as_str()) { + Some("ConfigMap") => DirectedYamlKind::ConfigMap, + Some("Secret") => DirectedYamlKind::Secret, + _ => { + return EventResult::Ignore; + } + }, + view_id::tab_network_widget_network => match metadata.get("kind").map(|v| v.as_str()) { + Some("Ingress") => DirectedYamlKind::Ingress, + Some("Service") => DirectedYamlKind::Service, + Some("Pod") => DirectedYamlKind::Pod, + Some("NetworkPolicy") => DirectedYamlKind::NetworkPolicy, + _ => { + return EventResult::Ignore; + } + }, + _ => return EventResult::Ignore, + }; + + tx.send( + YamlRequest::DirectedYaml(DirectedYaml { + name: name.to_string(), + namespace: namespace.to_string(), + kind, + }) + .into(), + ) + .expect("Failed to send YamlMessage::Request"); + + w.widget_clear(view_id::popup_yaml); + w.open_popup(view_id::popup_yaml); + + EventResult::Nop + } +} diff --git a/src/window/yaml.rs b/src/window/yaml.rs index 7794ac5e..a185813c 100644 --- a/src/window/yaml.rs +++ b/src/window/yaml.rs @@ -6,7 +6,10 @@ use std::{cell::RefCell, rc::Rc}; use crate::{ action::view_id, clipboard_wrapper::Clipboard, - event::{kubernetes::yaml::YamlRequest, Event}, + event::{ + kubernetes::yaml::{select::SelectedYaml, YamlRequest}, + Event, + }, logger, ui::{ event::EventResult, @@ -158,11 +161,11 @@ impl<'a> YamlTabBuilder<'a> { }; tx.send( - YamlRequest::Yaml { + YamlRequest::SelectedYaml(SelectedYaml { kind, name: name.to_string(), namespace: namespace.to_string(), - } + }) .into(), ) .expect("Failed to send YamlRequest::Yaml"); diff --git a/src/window/yaml_popup.rs b/src/window/yaml_popup.rs new file mode 100644 index 00000000..88c3cc9b --- /dev/null +++ b/src/window/yaml_popup.rs @@ -0,0 +1,45 @@ +use std::{cell::RefCell, rc::Rc}; + +use crate::{ + action::view_id, + clipboard_wrapper::Clipboard, + ui::widget::{config::WidgetConfig, Text, Widget, WidgetTrait}, +}; + +pub struct YamlPopup { + pub popup: Widget<'static>, +} + +pub struct YamlPopupBuilder<'a> { + clipboard: &'a Option>>, +} + +impl<'a> YamlPopupBuilder<'a> { + pub fn new(clipboard: &'a Option>>) -> Self { + Self { clipboard } + } + + pub fn build(&self) -> YamlPopup { + let mut builder = Text::builder() + .id(view_id::popup_yaml) + .widget_config(&WidgetConfig::builder().title("Yaml").build()) + .block_injection(|text: &Text, is_active: bool, is_mouse_over: bool| { + let (index, size) = text.state(); + + let mut config = text.widget_config().clone(); + + *config.title_mut() = format!("Yaml [{}/{}]", index, size).into(); + + config.render_block(text.can_activate() && is_active, is_mouse_over) + }) + .wrap(); + + if let Some(clipboard) = self.clipboard { + builder = builder.clipboard(clipboard.clone()); + } + + YamlPopup { + popup: builder.build().into(), + } + } +}