diff --git a/Cargo.lock b/Cargo.lock index 673252ec..dd807a50 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3642,6 +3642,7 @@ dependencies = [ "json-patch", "k8s-openapi", "kube", + "lazy_static", "ratatui", "reqwest 0.11.27", "rstest", diff --git a/Cargo.toml b/Cargo.toml index b0da3ae3..2f7c5ece 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -41,6 +41,7 @@ either = "1.12.0" futures = "0.3.28" json-patch = "1.2.0" k8s-openapi = { version = "0.19.0", features = ["v1_27"] } +lazy_static = "1.5.0" object_store = { version = "0.11.0", features = ["aws", "gcp", "azure", "http"] } # remove this fork once https://github.com/uutils/parse_datetime/pull/80 is merged and a new version released parse_datetime_fork = { version = "0.6.0-custom" } diff --git a/sk-cli/Cargo.toml b/sk-cli/Cargo.toml index 54979c7b..3f983571 100644 --- a/sk-cli/Cargo.toml +++ b/sk-cli/Cargo.toml @@ -22,6 +22,7 @@ dirs = { workspace = true } json-patch = { workspace = true } kube = { workspace = true } k8s-openapi = { workspace = true } +lazy_static = { workspace = true } ratatui = { workspace = true } reqwest = { workspace = true } serde = { workspace = true } diff --git a/sk-cli/src/validation/annotated_trace.rs b/sk-cli/src/validation/annotated_trace.rs index a62dbe88..e7e4ddf6 100644 --- a/sk-cli/src/validation/annotated_trace.rs +++ b/sk-cli/src/validation/annotated_trace.rs @@ -15,12 +15,24 @@ use sk_store::{ TraceStore, }; -use super::validator::Validator; +use super::validator::{ + Validator, + ValidatorCode, +}; #[derive(Clone, Default)] pub struct AnnotatedTraceEvent { pub data: TraceEvent, - pub annotations: Vec<(usize, String)>, + pub annotations: Vec>, +} + +impl AnnotatedTraceEvent { + pub fn new(data: TraceEvent) -> AnnotatedTraceEvent { + let len = data.applied_objs.len() + data.deleted_objs.len(); + let annotations = vec![vec![]; len]; + + AnnotatedTraceEvent { data, annotations } + } } #[derive(Default)] @@ -29,7 +41,7 @@ pub struct AnnotatedTrace { base: TraceStore, path: String, events: Vec, - summary: BTreeMap, + summary: BTreeMap, } impl AnnotatedTrace { @@ -37,10 +49,7 @@ impl AnnotatedTrace { let object_store = SkObjectStore::new(trace_path)?; let trace_data = object_store.get().await?.to_vec(); let base = TraceStore::import(trace_data, &None)?; - let events = base - .iter() - .map(|(event, _)| AnnotatedTraceEvent { data: event.clone(), ..Default::default() }) - .collect(); + let events = base.iter().map(|(event, _)| AnnotatedTraceEvent::new(event.clone())).collect(); Ok(AnnotatedTrace { base, events, @@ -49,16 +58,16 @@ impl AnnotatedTrace { }) } - pub fn validate(&mut self, validators: &mut BTreeMap) { - for evt in self.events.iter_mut() { + pub fn validate(&mut self, validators: &mut BTreeMap) { + for event in self.events.iter_mut() { for (code, validator) in validators.iter_mut() { - let mut affected_indices: Vec<_> = - validator.check_next_event(evt).into_iter().map(|i| (i, code.clone())).collect(); + let affected_indices = validator.check_next_event(event); let count = affected_indices.len(); - self.summary.entry(code.into()).and_modify(|e| *e += count).or_insert(count); + self.summary.entry(*code).and_modify(|e| *e += count).or_insert(count); - // This needs to happen at the ends, since `append` consumes affected_indices' contents - evt.annotations.append(&mut affected_indices); + for i in affected_indices { + event.annotations[i].push(*code); + } } } } @@ -79,7 +88,7 @@ impl AnnotatedTrace { self.events.first().map(|evt| evt.data.ts) } - pub fn summary_iter(&self) -> btree_map::Iter<'_, String, usize> { + pub fn summary_iter(&self) -> btree_map::Iter<'_, ValidatorCode, usize> { self.summary.iter() } } @@ -98,7 +107,7 @@ impl AnnotatedTrace { AnnotatedTrace { events, ..Default::default() } } - pub fn summary_for(&self, code: &str) -> Option { + pub fn summary_for(&self, code: &ValidatorCode) -> Option { self.summary.get(code).cloned() } } diff --git a/sk-cli/src/validation/mod.rs b/sk-cli/src/validation/mod.rs index 1c1627de..137d7a5f 100644 --- a/sk-cli/src/validation/mod.rs +++ b/sk-cli/src/validation/mod.rs @@ -10,8 +10,15 @@ use clap::{ }; use sk_core::prelude::*; -pub use self::annotated_trace::AnnotatedTrace; +pub use self::annotated_trace::{ + AnnotatedTrace, + AnnotatedTraceEvent, +}; pub use self::validation_store::ValidationStore; +pub use self::validator::{ + ValidatorCode, + ValidatorType, +}; #[derive(Subcommand)] pub enum ValidateSubcommand { @@ -33,8 +40,8 @@ pub struct CheckArgs { #[derive(clap::Args)] pub struct ExplainArgs { - #[arg(long_help = "Error code to explain")] - pub code: String, + #[arg(long_help = "Error code to explain", value_parser = ValidatorCode::parse)] + pub code: ValidatorCode, } #[derive(Clone, ValueEnum)] diff --git a/sk-cli/src/validation/tests/mod.rs b/sk-cli/src/validation/tests/mod.rs index e0feb2fa..67ef24c2 100644 --- a/sk-cli/src/validation/tests/mod.rs +++ b/sk-cli/src/validation/tests/mod.rs @@ -1,43 +1,72 @@ mod status_field_populated_test; mod validation_store_test; +use std::collections::BTreeMap; + use rstest::*; use sk_core::k8s::testutils::test_deployment; use sk_store::TraceEvent; use super::annotated_trace::AnnotatedTraceEvent; +use super::validator::{ + Diagnostic, + Validator, + ValidatorCode, + ValidatorType, +}; use super::*; +const TEST_VALIDATOR_CODE: ValidatorCode = ValidatorCode(ValidatorType::Error, 9999); + #[fixture] pub fn annotated_trace() -> AnnotatedTrace { AnnotatedTrace::new_with_events(vec![ - AnnotatedTraceEvent { - data: TraceEvent { ts: 0, ..Default::default() }, - ..Default::default() - }, - AnnotatedTraceEvent { - data: TraceEvent { - ts: 1, - applied_objs: vec![test_deployment("test_depl1")], - deleted_objs: vec![], - }, - ..Default::default() - }, - AnnotatedTraceEvent { - data: TraceEvent { - ts: 2, - applied_objs: vec![test_deployment("test_depl1"), test_deployment("test_depl2")], - deleted_objs: vec![], - }, - ..Default::default() - }, - AnnotatedTraceEvent { - data: TraceEvent { - ts: 3, - applied_objs: vec![], - deleted_objs: vec![test_deployment("test_depl1")], - }, - ..Default::default() - }, + AnnotatedTraceEvent::new(TraceEvent { ts: 0, ..Default::default() }), + AnnotatedTraceEvent::new(TraceEvent { + ts: 1, + applied_objs: vec![test_deployment("test_depl1")], + deleted_objs: vec![], + }), + AnnotatedTraceEvent::new(TraceEvent { + ts: 2, + applied_objs: vec![test_deployment("test_depl1"), test_deployment("test_depl2")], + deleted_objs: vec![], + }), + AnnotatedTraceEvent::new(TraceEvent { + ts: 3, + applied_objs: vec![], + deleted_objs: vec![test_deployment("test_depl1")], + }), ]) } + +struct TestDiagnostic {} + +impl Diagnostic for TestDiagnostic { + fn check_next_event(&mut self, evt: &mut AnnotatedTraceEvent) -> Vec { + if evt.data.applied_objs.len() > 1 { + vec![1] + } else { + vec![] + } + } + + fn reset(&mut self) {} +} + +#[fixture] +fn test_validator() -> Validator { + Validator { + type_: ValidatorType::Warning, + name: "test_validator", + help: "HELP ME, I'M STUCK IN THE BORROW CHECKER", + diagnostic: Box::new(TestDiagnostic {}), + } +} + +#[fixture] +pub fn test_validation_store(test_validator: Validator) -> ValidationStore { + let mut test_store = ValidationStore { validators: BTreeMap::new() }; + test_store.register_with_code(TEST_VALIDATOR_CODE, test_validator); + test_store +} diff --git a/sk-cli/src/validation/tests/validation_store_test.rs b/sk-cli/src/validation/tests/validation_store_test.rs index 079e6ceb..17414e4b 100644 --- a/sk-cli/src/validation/tests/validation_store_test.rs +++ b/sk-cli/src/validation/tests/validation_store_test.rs @@ -1,53 +1,20 @@ -use std::collections::BTreeMap; - use assertables::*; -use super::validator::{ - Diagnostic, - Validator, - ValidatorType, -}; use super::*; -struct TestDiagnostic {} - -impl Diagnostic for TestDiagnostic { - fn check_next_event(&mut self, evt: &mut AnnotatedTraceEvent) -> Vec { - if evt.data.applied_objs.len() > 1 { - vec![1] - } else { - vec![] - } - } - - fn reset(&mut self) {} -} - -#[fixture] -fn validator() -> Validator { - Validator { - type_: ValidatorType::Warning, - name: "test_validator", - help: "HELP ME, I'M STUCK IN THE BORROW CHECKER", - diagnostic: Box::new(TestDiagnostic {}), - } -} - #[rstest] -fn test_validate_trace(validator: Validator, mut annotated_trace: AnnotatedTrace) { - let code = "W9999"; - let mut test_store = ValidationStore { validators: BTreeMap::new() }; - test_store.register_with_code(code.into(), validator); - - test_store.validate_trace(&mut annotated_trace); +fn test_validate_trace(mut test_validation_store: ValidationStore, mut annotated_trace: AnnotatedTrace) { + test_validation_store.validate_trace(&mut annotated_trace); for evt in annotated_trace.iter() { if evt.data.applied_objs.len() > 1 { - assert_eq!(evt.annotations, vec![(1, code.into())]); + assert_eq!(evt.annotations[1], vec![TEST_VALIDATOR_CODE]); } else { - assert_is_empty!(evt.annotations); + for annotation in &evt.annotations { + assert_is_empty!(annotation); + } } } - assert_eq!(annotated_trace.summary_for(code).unwrap(), 1); + assert_eq!(annotated_trace.summary_for(&TEST_VALIDATOR_CODE).unwrap(), 1); } diff --git a/sk-cli/src/validation/validation_store.rs b/sk-cli/src/validation/validation_store.rs index 838cf5e9..bcaf981a 100644 --- a/sk-cli/src/validation/validation_store.rs +++ b/sk-cli/src/validation/validation_store.rs @@ -5,7 +5,10 @@ use serde::Serialize; use sk_core::prelude::*; use super::annotated_trace::AnnotatedTrace; -use super::validator::Validator; +use super::validator::{ + Validator, + ValidatorCode, +}; use super::{ status_field_populated, PrintFormat, @@ -13,7 +16,7 @@ use super::{ #[derive(Serialize)] pub struct ValidationStore { - pub(super) validators: BTreeMap, + pub(super) validators: BTreeMap, } impl ValidationStore { @@ -25,7 +28,7 @@ impl ValidationStore { trace.validate(&mut self.validators); } - pub(super) fn explain(&self, code: &str) -> EmptyResult { + pub(super) fn explain(&self, code: &ValidatorCode) -> EmptyResult { let v = self.lookup(code)?; println!("{} ({code})", v.name); println!("{:=<80}", ""); @@ -33,7 +36,7 @@ impl ValidationStore { Ok(()) } - pub(super) fn lookup<'a>(&'a self, code: &str) -> anyhow::Result<&'a Validator> { + pub(super) fn lookup<'a>(&'a self, code: &ValidatorCode) -> anyhow::Result<&'a Validator> { self.validators.get(code).ok_or(anyhow!("code not found: {code}")) } @@ -49,11 +52,11 @@ impl ValidationStore { } pub(super) fn register(&mut self, v: Validator) { - let code = format!("{}{:04}", v.type_, self.validators.len()); + let code = ValidatorCode(v.type_, self.validators.len()); self.register_with_code(code, v); } - pub(super) fn register_with_code(&mut self, code: String, v: Validator) { + pub(super) fn register_with_code(&mut self, code: ValidatorCode, v: Validator) { self.validators.insert(code, v); } diff --git a/sk-cli/src/validation/validator.rs b/sk-cli/src/validation/validator.rs index 61350b3b..fad1ef7a 100644 --- a/sk-cli/src/validation/validator.rs +++ b/sk-cli/src/validation/validator.rs @@ -1,5 +1,7 @@ use std::fmt; +use std::str::from_utf8; +use anyhow::bail; use serde::{ Serialize, Serializer, @@ -7,22 +9,42 @@ use serde::{ use super::annotated_trace::AnnotatedTraceEvent; -#[derive(Eq, Hash, PartialEq, Serialize)] +#[derive(Clone, Copy, Debug, Eq, Ord, PartialEq, PartialOrd, Serialize)] pub enum ValidatorType { Warning, - #[allow(dead_code)] Error, } -impl fmt::Display for ValidatorType { +#[derive(Clone, Copy, Debug, Eq, Ord, PartialEq, PartialOrd, Serialize)] +pub struct ValidatorCode(pub ValidatorType, pub usize); + +impl ValidatorCode { + pub fn parse(s: &str) -> anyhow::Result { + if s.is_empty() { + bail!("empty string"); + } + + let chars = s.as_bytes(); + let t = match chars[0] { + b'W' => ValidatorType::Warning, + b'E' => ValidatorType::Error, + _ => bail!("unknown type"), + }; + let id = from_utf8(&chars[1..])?.parse::()?; + Ok(ValidatorCode(t, id)) + } +} + +impl fmt::Display for ValidatorCode { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { write!( f, - "{}", - match self { + "{}{:04}", + match self.0 { ValidatorType::Warning => 'W', ValidatorType::Error => 'E', - } + }, + self.1, ) } } @@ -62,3 +84,18 @@ impl Validator { fn flatten_str(s: &str, ser: S) -> Result { ser.serialize_str(&s.replace('\n', " ")) } + +#[cfg(test)] +mod tests { + use assertables::*; + use rstest::*; + + use super::*; + + #[rstest] + fn test_parse_validator_code() { + assert_eq!(ValidatorCode::parse("E0001").unwrap(), ValidatorCode(ValidatorType::Error, 1)); + assert_eq!(ValidatorCode::parse("W0001").unwrap(), ValidatorCode(ValidatorType::Warning, 1)); + assert_err!(ValidatorCode::parse("asdf")); + } +} diff --git a/sk-cli/src/xray/mod.rs b/sk-cli/src/xray/mod.rs index 0e7d4c06..5f1e88b4 100644 --- a/sk-cli/src/xray/mod.rs +++ b/sk-cli/src/xray/mod.rs @@ -1,6 +1,5 @@ mod app; mod event; -mod util; mod view; use ratatui::backend::Backend; diff --git a/sk-cli/src/xray/tests/snapshots/skctl__xray__tests__view_test__itest_render_event_list@0.snap b/sk-cli/src/xray/tests/snapshots/skctl__xray__tests__view_test__itest_render_event_list@0.snap index d88d597c..da79c915 100644 --- a/sk-cli/src/xray/tests/snapshots/skctl__xray__tests__view_test__itest_render_event_list@0.snap +++ b/sk-cli/src/xray/tests/snapshots/skctl__xray__tests__view_test__itest_render_event_list@0.snap @@ -9,7 +9,7 @@ CompletedFrame { "┌──────────────────────────────────────────────────────────────────────────────┐", "│>> 00:00:00 (0 applied/0 deleted) │", "│ 00:00:01 (1 applied/0 deleted) │", - "│ 00:00:02 (2 applied/0 deleted) │", + "│ 00:00:02 (2 applied/0 deleted) 1 error/0 warnings │", "│ 00:00:03 (0 applied/1 deleted) │", "│ │", "│ │", @@ -31,6 +31,8 @@ CompletedFrame { x: 0, y: 0, fg: Reset, bg: Reset, underline: Reset, modifier: NONE, x: 1, y: 1, fg: Reset, bg: Reset, underline: Reset, modifier: REVERSED, x: 79, y: 1, fg: Reset, bg: Reset, underline: Reset, modifier: NONE, + x: 56, y: 3, fg: White, bg: Red, underline: Reset, modifier: NONE, + x: 79, y: 3, fg: Reset, bg: Reset, underline: Reset, modifier: NONE, x: 0, y: 15, fg: White, bg: Reset, underline: Reset, modifier: NONE, ] }, diff --git a/sk-cli/src/xray/tests/snapshots/skctl__xray__tests__view_test__itest_render_event_list@3.snap b/sk-cli/src/xray/tests/snapshots/skctl__xray__tests__view_test__itest_render_event_list@3.snap index f5723bc3..35edbdef 100644 --- a/sk-cli/src/xray/tests/snapshots/skctl__xray__tests__view_test__itest_render_event_list@3.snap +++ b/sk-cli/src/xray/tests/snapshots/skctl__xray__tests__view_test__itest_render_event_list@3.snap @@ -9,7 +9,7 @@ CompletedFrame { "┌──────────────────────────────────────────────────────────────────────────────┐", "│ 00:00:00 (0 applied/0 deleted) │", "│ 00:00:01 (1 applied/0 deleted) │", - "│ 00:00:02 (2 applied/0 deleted) │", + "│ 00:00:02 (2 applied/0 deleted) 1 error/0 warnings │", "│>> 00:00:03 (0 applied/1 deleted) │", "│ │", "│ │", @@ -29,6 +29,8 @@ CompletedFrame { ], styles: [ x: 0, y: 0, fg: Reset, bg: Reset, underline: Reset, modifier: NONE, + x: 56, y: 3, fg: White, bg: Red, underline: Reset, modifier: NONE, + x: 79, y: 3, fg: Reset, bg: Reset, underline: Reset, modifier: NONE, x: 1, y: 4, fg: Reset, bg: Reset, underline: Reset, modifier: REVERSED, x: 79, y: 4, fg: Reset, bg: Reset, underline: Reset, modifier: NONE, x: 0, y: 15, fg: White, bg: Reset, underline: Reset, modifier: NONE, diff --git a/sk-cli/src/xray/tests/snapshots/skctl__xray__tests__view_test__itest_render_event_list_event_selected@0.snap b/sk-cli/src/xray/tests/snapshots/skctl__xray__tests__view_test__itest_render_event_list_event_selected@0.snap index 2e95187a..a203f9da 100644 --- a/sk-cli/src/xray/tests/snapshots/skctl__xray__tests__view_test__itest_render_event_list_event_selected@0.snap +++ b/sk-cli/src/xray/tests/snapshots/skctl__xray__tests__view_test__itest_render_event_list_event_selected@0.snap @@ -10,7 +10,7 @@ CompletedFrame { "│>> 00:00:00 (0 applied/0 deleted) │", "│++ │", "│ 00:00:01 (1 applied/0 deleted) │", - "│ 00:00:02 (2 applied/0 deleted) │", + "│ 00:00:02 (2 applied/0 deleted) 1 error/0 warnings │", "│ 00:00:03 (0 applied/1 deleted) │", "│ │", "│ │", @@ -31,8 +31,10 @@ CompletedFrame { x: 0, y: 0, fg: Reset, bg: Reset, underline: Reset, modifier: NONE, x: 1, y: 1, fg: Reset, bg: Reset, underline: Reset, modifier: REVERSED, x: 79, y: 1, fg: Reset, bg: Reset, underline: Reset, modifier: NONE, - x: 1, y: 2, fg: Reset, bg: Blue, underline: Reset, modifier: ITALIC, + x: 1, y: 2, fg: Reset, bg: Blue, underline: Reset, modifier: NONE, x: 79, y: 2, fg: Reset, bg: Reset, underline: Reset, modifier: NONE, + x: 56, y: 4, fg: White, bg: Red, underline: Reset, modifier: NONE, + x: 79, y: 4, fg: Reset, bg: Reset, underline: Reset, modifier: NONE, x: 0, y: 15, fg: White, bg: Reset, underline: Reset, modifier: NONE, ] }, diff --git a/sk-cli/src/xray/tests/snapshots/skctl__xray__tests__view_test__itest_render_event_list_event_selected@2.snap b/sk-cli/src/xray/tests/snapshots/skctl__xray__tests__view_test__itest_render_event_list_event_selected@2.snap index 8142911b..12f118ca 100644 --- a/sk-cli/src/xray/tests/snapshots/skctl__xray__tests__view_test__itest_render_event_list_event_selected@2.snap +++ b/sk-cli/src/xray/tests/snapshots/skctl__xray__tests__view_test__itest_render_event_list_event_selected@2.snap @@ -9,9 +9,9 @@ CompletedFrame { "┌──────────────────────────────────────────────────────────────────────────────┐", "│ 00:00:00 (0 applied/0 deleted) │", "│ 00:00:01 (1 applied/0 deleted) │", - "│>> 00:00:02 (2 applied/0 deleted) │", + "│>> 00:00:02 (2 applied/0 deleted) 1 error/0 warnings │", "│++ + test-namespace/test_depl1 │", - "│ + test-namespace/test_depl2 │", + "│ + test-namespace/test_depl2 1 error/0 warnings │", "│ 00:00:03 (0 applied/1 deleted) │", "│ │", "│ │", @@ -30,10 +30,15 @@ CompletedFrame { styles: [ x: 0, y: 0, fg: Reset, bg: Reset, underline: Reset, modifier: NONE, x: 1, y: 3, fg: Reset, bg: Reset, underline: Reset, modifier: REVERSED, + x: 56, y: 3, fg: White, bg: Red, underline: Reset, modifier: REVERSED, x: 79, y: 3, fg: Reset, bg: Reset, underline: Reset, modifier: NONE, - x: 1, y: 4, fg: Reset, bg: Blue, underline: Reset, modifier: ITALIC, + x: 1, y: 4, fg: Reset, bg: Blue, underline: Reset, modifier: NONE, + x: 4, y: 4, fg: Reset, bg: Blue, underline: Reset, modifier: ITALIC, + x: 33, y: 4, fg: Reset, bg: Blue, underline: Reset, modifier: NONE, x: 79, y: 4, fg: Reset, bg: Reset, underline: Reset, modifier: NONE, - x: 1, y: 5, fg: Reset, bg: Reset, underline: Reset, modifier: ITALIC, + x: 4, y: 5, fg: Reset, bg: Reset, underline: Reset, modifier: ITALIC, + x: 33, y: 5, fg: Reset, bg: Reset, underline: Reset, modifier: NONE, + x: 56, y: 5, fg: White, bg: Red, underline: Reset, modifier: NONE, x: 79, y: 5, fg: Reset, bg: Reset, underline: Reset, modifier: NONE, x: 0, y: 15, fg: White, bg: Reset, underline: Reset, modifier: NONE, ] diff --git a/sk-cli/src/xray/tests/snapshots/skctl__xray__tests__view_test__itest_render_event_list_event_selected@3.snap b/sk-cli/src/xray/tests/snapshots/skctl__xray__tests__view_test__itest_render_event_list_event_selected@3.snap index 6a4c39c7..484115ec 100644 --- a/sk-cli/src/xray/tests/snapshots/skctl__xray__tests__view_test__itest_render_event_list_event_selected@3.snap +++ b/sk-cli/src/xray/tests/snapshots/skctl__xray__tests__view_test__itest_render_event_list_event_selected@3.snap @@ -9,7 +9,7 @@ CompletedFrame { "┌──────────────────────────────────────────────────────────────────────────────┐", "│ 00:00:00 (0 applied/0 deleted) │", "│ 00:00:01 (1 applied/0 deleted) │", - "│ 00:00:02 (2 applied/0 deleted) │", + "│ 00:00:02 (2 applied/0 deleted) 1 error/0 warnings │", "│>> 00:00:03 (0 applied/1 deleted) │", "│++ - test-namespace/test_depl1 │", "│ │", @@ -29,9 +29,13 @@ CompletedFrame { ], styles: [ x: 0, y: 0, fg: Reset, bg: Reset, underline: Reset, modifier: NONE, + x: 56, y: 3, fg: White, bg: Red, underline: Reset, modifier: NONE, + x: 79, y: 3, fg: Reset, bg: Reset, underline: Reset, modifier: NONE, x: 1, y: 4, fg: Reset, bg: Reset, underline: Reset, modifier: REVERSED, x: 79, y: 4, fg: Reset, bg: Reset, underline: Reset, modifier: NONE, - x: 1, y: 5, fg: Reset, bg: Blue, underline: Reset, modifier: ITALIC, + x: 1, y: 5, fg: Reset, bg: Blue, underline: Reset, modifier: NONE, + x: 4, y: 5, fg: Reset, bg: Blue, underline: Reset, modifier: ITALIC, + x: 33, y: 5, fg: Reset, bg: Blue, underline: Reset, modifier: NONE, x: 79, y: 5, fg: Reset, bg: Reset, underline: Reset, modifier: NONE, x: 0, y: 15, fg: White, bg: Reset, underline: Reset, modifier: NONE, ] diff --git a/sk-cli/src/xray/tests/view_test.rs b/sk-cli/src/xray/tests/view_test.rs index ddd99b5f..5b2f3729 100644 --- a/sk-cli/src/xray/tests/view_test.rs +++ b/sk-cli/src/xray/tests/view_test.rs @@ -4,11 +4,18 @@ use ratatui::prelude::*; use ratatui::widgets::ListState; use super::*; -use crate::validation::tests::annotated_trace; -use crate::validation::AnnotatedTrace; +use crate::validation::tests::{ + annotated_trace, + test_validation_store, +}; +use crate::validation::{ + AnnotatedTrace, + ValidationStore, +}; #[fixture] -fn test_app(annotated_trace: AnnotatedTrace) -> App { +fn test_app(mut test_validation_store: ValidationStore, mut annotated_trace: AnnotatedTrace) -> App { + test_validation_store.validate_trace(&mut annotated_trace); App { annotated_trace, event_list_state: ListState::default().with_selected(Some(0)), diff --git a/sk-cli/src/xray/util.rs b/sk-cli/src/xray/util.rs deleted file mode 100644 index 1fa074cc..00000000 --- a/sk-cli/src/xray/util.rs +++ /dev/null @@ -1,10 +0,0 @@ -use chrono::TimeDelta; - -pub(super) fn format_duration(d: TimeDelta) -> String { - let day_str = match d.num_days() { - x if x > 0 => format!("{x}d "), - _ => String::new(), - }; - - format!("{}{:02}:{:02}:{:02}", day_str, d.num_hours() % 24, d.num_minutes() % 60, d.num_seconds() % 60) -} diff --git a/sk-cli/src/xray/view/helpers.rs b/sk-cli/src/xray/view/helpers.rs new file mode 100644 index 00000000..e087b432 --- /dev/null +++ b/sk-cli/src/xray/view/helpers.rs @@ -0,0 +1,106 @@ +use chrono::TimeDelta; +use kube::api::DynamicObject; +use lazy_static::lazy_static; +use ratatui::prelude::*; +use sk_core::k8s::KubeResourceExt; + +use crate::validation::{ + AnnotatedTraceEvent, + ValidatorType, +}; + +pub(super) const LIST_PADDING: usize = 3; +lazy_static! { + static ref ERR_STYLE: Style = Style::new().white().on_red(); + static ref WARN_STYLE: Style = Style::new().white().on_yellow(); +} + +pub(super) fn make_event_spans(event: &AnnotatedTraceEvent, start_ts: i64) -> (Span, Span) { + let d = TimeDelta::new(event.data.ts - start_ts, 0).unwrap(); + let evt_span = Span::from(format!( + "{} ({} applied/{} deleted)", + format_duration(d), + event.data.applied_objs.len(), + event.data.deleted_objs.len() + )); + + let (warnings, errs) = event.annotations.iter().fold((0, 0), |(mut w, mut e), codes| { + for code in codes { + match code.0 { + ValidatorType::Warning => w += 1, + ValidatorType::Error => e += 1, + } + } + (w, e) + }); + + if warnings + errs == 0 { + (evt_span, Span::default()) + } else { + let err_str = format!("{} error{}", errs, if errs == 1 { "" } else { "s" }); + let warn_str = format!("{} warning{}", warnings, if warnings == 1 { "" } else { "s" }); + let style = if errs == 0 { *WARN_STYLE } else { *ERR_STYLE }; + let err_span = Span::styled(format!(" {err_str}/{warn_str} "), style); + + (evt_span, err_span) + } +} + +pub(super) fn make_object_spans<'a>( + index: usize, + obj: &DynamicObject, + op: char, + event: &'a AnnotatedTraceEvent, +) -> (Span<'a>, Span<'a>) { + let obj_span = Span::styled(format!(" {} {}", op, obj.namespaced_name()), Style::new().italic()); + let (warnings, errs) = event.annotations[index].iter().fold((0, 0), |(mut w, mut e), code| { + match code.0 { + ValidatorType::Warning => w += 1, + ValidatorType::Error => e += 1, + } + (w, e) + }); + if warnings + errs == 0 { + (obj_span, Span::default()) + } else { + let err_str = format!("{} error{}", errs, if errs == 1 { "" } else { "s" }); + let warn_str = format!("{} warning{}", warnings, if warnings == 1 { "" } else { "s" }); + let style = if errs == 0 { *WARN_STYLE } else { *ERR_STYLE }; + let err_span = Span::styled(format!(" {err_str}/{warn_str} "), style); + + (obj_span, err_span) + } +} + +pub(super) fn format_list_entries<'a>( + spans: impl Iterator, Span<'a>)> + Clone, + width: usize, +) -> Vec> { + let max_err_width = spans + .clone() + .max_by_key(|(_, err_span)| err_span.width()) + .map_or(0, |(_, err_span)| err_span.width()); + + spans + .map(|(evt_span, err_span)| { + let mid_padding_width = width - evt_span.width() - max_err_width - (LIST_PADDING * 2); + let mid_padding_span = Span::from(" ".repeat(mid_padding_width)); + let right_padding_width = match err_span.width() { + 0 => 0, + x => max_err_width - x + LIST_PADDING, + }; + let right_padding_span = Span::styled(" ".repeat(right_padding_width), err_span.style); + + Text::from(evt_span.clone() + mid_padding_span + err_span.clone() + right_padding_span) + }) + .collect() +} + +pub(super) fn format_duration(d: TimeDelta) -> String { + let day_str = match d.num_days() { + x if x > 0 => format!("{x}d "), + _ => String::new(), + }; + + format!("{}{:02}:{:02}:{:02}", day_str, d.num_hours() % 24, d.num_minutes() % 60, d.num_seconds() % 60) +} diff --git a/sk-cli/src/xray/view.rs b/sk-cli/src/xray/view/mod.rs similarity index 75% rename from sk-cli/src/xray/view.rs rename to sk-cli/src/xray/view/mod.rs index 27e53a50..8de7baae 100644 --- a/sk-cli/src/xray/view.rs +++ b/sk-cli/src/xray/view/mod.rs @@ -1,6 +1,10 @@ -use std::iter::repeat; +mod helpers; + +use std::iter::{ + once, + repeat, +}; -use chrono::TimeDelta; use ratatui::prelude::*; use ratatui::widgets::{ Block, @@ -10,13 +14,14 @@ use ratatui::widgets::{ Padding, Paragraph, }; -use sk_core::k8s::KubeResourceExt; +use self::helpers::*; use super::app::{ App, Mode, }; -use super::util::format_duration; + +const MIN_TWO_PANEL_WIDTH: u16 = 120; pub(super) fn view(app: &mut App, frame: &mut Frame) { let layout = Layout::default() @@ -28,7 +33,7 @@ pub(super) fn view(app: &mut App, frame: &mut Frame) { let events_border = Block::bordered().title(app.annotated_trace.path()); let object_border = Block::bordered(); - if top.width > 120 { + if top.width > MIN_TWO_PANEL_WIDTH { let lr_layout = Layout::default() .direction(Direction::Horizontal) .constraints(vec![Constraint::Percentage(50), Constraint::Percentage(50)]) @@ -83,37 +88,25 @@ fn render_event_list(app: &mut App, frame: &mut Frame, layout: Rect) { _ => (num_events, None), }; - let mut root_items_1 = Vec::with_capacity(sel_index_inclusive); - let mut root_items_2 = Vec::with_capacity(num_events - sel_index_inclusive); - - for (i, evt) in app.annotated_trace.iter().enumerate() { - let d = TimeDelta::new(evt.data.ts - start_ts, 0).unwrap(); - let d_str = format!( - "{} ({} applied/{} deleted)", - format_duration(d), - evt.data.applied_objs.len(), - evt.data.deleted_objs.len() - ); - if i < sel_index_inclusive { - root_items_1.push(d_str); - } else { - root_items_2.push(d_str); - } - } + let event_spans = app.annotated_trace.iter().map(|event| make_event_spans(event, start_ts)); + let mut top_entries = format_list_entries(event_spans, layout.width as usize); + let bottom_entries = top_entries.split_off(sel_index_inclusive); - let sublist_items = sel_event.map_or(vec![], |evt| { - let mut items: Vec<_> = evt + let obj_spans = sel_event.map_or(vec![], |evt| { + let mut sublist_items = evt .data .applied_objs .iter() - .zip(repeat("+")) - .chain(evt.data.deleted_objs.iter().zip(repeat("-"))) - .map(|(obj, op)| format!(" {} {}", op, obj.namespaced_name())) - .collect(); - if items.is_empty() { - items.push(String::new()); + .zip(repeat('+')) + .chain(evt.data.deleted_objs.iter().zip(repeat('-'))) + .enumerate() + .map(|(i, (obj, op))| make_object_spans(i, obj, op, evt)) + .peekable(); + if sublist_items.peek().is_none() { + format_list_entries(once((Span::default(), Span::default())), layout.width as usize) + } else { + format_list_entries(sublist_items, layout.width as usize) } - items }); let nested_layout = Layout::default() @@ -121,20 +114,19 @@ fn render_event_list(app: &mut App, frame: &mut Frame, layout: Rect) { .constraints(vec![ // We know how many lines we have; use max constraints here so the lists are next to // each other. The last one can be min(0) and take up the rest of the space - Constraint::Max(root_items_1.len() as u16), - Constraint::Max(sublist_items.len() as u16), + Constraint::Max(top_entries.len() as u16), + Constraint::Max(obj_spans.len() as u16), Constraint::Min(0), ]) .split(layout); - let list_part_one = List::new(root_items_1) + let list_part_one = List::new(top_entries) .highlight_style(Style::new().add_modifier(Modifier::REVERSED)) .highlight_symbol(">> "); - let sublist = List::new(sublist_items) + let sublist = List::new(obj_spans) .highlight_style(Style::new().bg(Color::Blue)) - .highlight_symbol("++ ") - .style(Style::new().italic()); - let list_part_two = List::new(root_items_2).block(Block::new().padding(Padding::left(3))); + .highlight_symbol("++ "); + let list_part_two = List::new(bottom_entries).block(Block::new().padding(Padding::left(LIST_PADDING as u16))); frame.render_stateful_widget(list_part_one, nested_layout[0], &mut app.event_list_state); frame.render_stateful_widget(sublist, nested_layout[1], &mut app.object_list_state);