init commit

This commit is contained in:
Jack Wills
2022-04-25 02:44:39 +00:00
commit 5101f60aaa
28 changed files with 3289 additions and 0 deletions
+425
View File
@@ -0,0 +1,425 @@
use std::{cmp::Ordering, collections::VecDeque, fmt};
use tui::{
style::Color,
widgets::{ListItem, ListState},
};
#[derive(Debug, Clone)]
pub struct StatefulList<T> {
pub state: ListState,
pub items: Vec<T>,
}
impl<T> StatefulList<T> {
pub fn new(items: Vec<T>) -> Self {
Self {
state: ListState::default(),
items,
}
}
pub fn end(&mut self) {
let len = self.items.len();
if len > 0 {
self.state.select(Some(self.items.len() - 1));
}
}
pub fn start(&mut self) {
self.state.select(Some(0));
}
pub fn next(&mut self) {
if !self.items.is_empty() {
let i = match self.state.selected() {
Some(i) => {
if i < self.items.len() - 1 {
i + 1
} else {
i
}
}
None => 0,
};
self.state.select(Some(i));
}
}
pub fn previous(&mut self) {
if !self.items.is_empty() {
let i = match self.state.selected() {
Some(i) => {
if i == 0 {
0
} else {
i - 1
}
}
None => 0,
};
self.state.select(Some(i));
}
}
pub fn get_state_title(&self) -> String {
if self.items.is_empty() {
String::from("")
} else {
let len = self.items.len();
let c = if let Some(value) = self.state.selected() {
if len > 0 {
value + 1
} else {
value
}
} else {
0
};
format!("{}/{}", c, self.items.len())
}
}
}
/// States of the container
#[derive(Clone, Debug, PartialEq, PartialOrd)]
pub enum State {
Dead,
Exited,
Paused,
Removing,
Restarting,
Running,
Unknown,
}
impl State {
pub fn get_color(&self) -> Color {
match self {
Self::Running => Color::Green,
Self::Removing => Color::LightRed,
Self::Restarting => Color::LightGreen,
Self::Paused => Color::Yellow,
_ => Color::Red,
}
}
}
impl From<&str> for State {
fn from(input: &str) -> Self {
match input {
"dead" => Self::Dead,
"exited" => Self::Exited,
"paused" => Self::Paused,
"removing" => Self::Removing,
"restarting" => Self::Restarting,
"running" => Self::Running,
_ => Self::Unknown,
}
}
}
impl fmt::Display for State {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
let disp = match self {
Self::Dead => "✖ dead",
Self::Exited => "✖ exited",
Self::Paused => "॥ paused",
Self::Removing => "removing",
Self::Restarting => "↻ restarting",
Self::Running => "✓ running",
Self::Unknown => "? unknown",
};
write!(f, "{}", disp)
}
}
/// Items for the container control list
/// Should probably have a vec for each container
/// so that can remove Pause if container currently Paused etc
#[derive(Debug, Clone)]
pub enum DockerControls {
Pause,
Unpause,
Restart,
Stop,
Start,
}
impl DockerControls {
pub fn get_color(&self) -> Color {
match self {
Self::Start => Color::Green,
Self::Stop => Color::Red,
Self::Restart => Color::Magenta,
Self::Pause => Color::Yellow,
Self::Unpause => Color::Blue,
}
}
pub fn gen_vec(state: &State) -> Vec<Self> {
match state {
State::Dead | State::Exited => vec![Self::Start, Self::Restart],
State::Paused => vec![Self::Unpause, Self::Stop],
State::Running => vec![Self::Pause, Self::Restart, Self::Stop],
_ => vec![],
}
}
}
impl fmt::Display for DockerControls {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
let disp = match self {
Self::Pause => "pause",
Self::Unpause => "unpause",
Self::Restart => "restart",
Self::Stop => "stop",
Self::Start => "start",
};
write!(f, "{}", disp)
}
}
pub trait Stats {
fn get_value(&self) -> f64;
}
/// Struct for frequently updated CPU stats
/// So can use custom display formatter
/// Use trait Stats for use as generic in draw_chart function
#[derive(Clone, Debug)]
pub struct CpuStats {
value: f64,
}
impl CpuStats {
pub fn new(value: f64) -> Self {
Self { value }
}
}
impl Eq for CpuStats {}
impl PartialEq for CpuStats {
fn eq(&self, other: &Self) -> bool {
self.value == other.value
}
}
impl PartialOrd for CpuStats {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
self.value.partial_cmp(&other.value)
}
}
impl Ord for CpuStats {
fn cmp(&self, other: &Self) -> Ordering {
if self.value > other.value {
Ordering::Greater
} else if self.value == other.value {
Ordering::Equal
} else {
Ordering::Less
}
}
}
impl Stats for CpuStats {
fn get_value(&self) -> f64 {
self.value
}
}
impl fmt::Display for CpuStats {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
let disp = format!("{:05.2}%", self.value);
write!(f, "{:>x$}", disp, x = f.width().unwrap_or(1))
}
}
/// Struct for frequently updated memory usage stats
/// So can use custom display formatter
/// Use trait Stats for use as generic in draw_chart function
#[derive(Clone, Debug, Eq)]
pub struct ByteStats {
value: u64,
}
impl PartialEq for ByteStats {
fn eq(&self, other: &Self) -> bool {
self.value == other.value
}
}
impl PartialOrd for ByteStats {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
self.value.partial_cmp(&other.value)
}
}
impl Ord for ByteStats {
fn cmp(&self, other: &Self) -> Ordering {
self.value.cmp(&other.value)
}
}
impl ByteStats {
pub fn new(value: u64) -> Self {
Self { value }
}
pub fn update(&mut self, value: u64) {
self.value = value;
}
}
impl Stats for ByteStats {
fn get_value(&self) -> f64 {
self.value as f64
}
}
// convert from bytes to kb, mb, gb etc
impl fmt::Display for ByteStats {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
let one_kb = 1000.0;
let one_mb = one_kb * one_kb;
let one_gb = one_mb * 1000.0;
let as_f64 = self.value as f64;
let p = match as_f64 {
x if x >= one_gb => format!("{y:.2} GB", y = as_f64 / one_gb),
x if x >= one_kb => format!("{y:.2} MB", y = as_f64 / one_mb),
x if x >= one_mb => format!("{y:.2} kB", y = as_f64 / one_kb),
_ => format!("{} B", self.value),
};
write!(f, "{:>x$}", p, x = f.width().unwrap_or(1))
}
}
/// Info for each container
#[derive(Debug, Clone)]
pub struct ContainerItem {
pub cpu_stats: VecDeque<CpuStats>,
pub docker_controls: StatefulList<DockerControls>,
pub id: String,
pub image: String,
pub last_updated: u64,
pub logs: StatefulList<ListItem<'static>>,
pub mem_limit: ByteStats,
pub mem_stats: VecDeque<ByteStats>,
pub name: String,
pub net_rx: ByteStats,
pub net_tx: ByteStats,
pub state: State,
pub status: String,
}
pub type MemTuple = (Vec<(f64, f64)>, ByteStats, State);
pub type CpuTuple = (Vec<(f64, f64)>, CpuStats, State);
impl ContainerItem {
/// Create a new container item
pub fn new(id: String, status: String, image: String, state: State, name: String) -> Self {
let mut docker_controls = StatefulList::new(DockerControls::gen_vec(&state));
docker_controls.start();
Self {
cpu_stats: VecDeque::with_capacity(60),
docker_controls,
id,
image,
last_updated: 0,
logs: StatefulList::new(vec![]),
mem_limit: ByteStats::new(0),
mem_stats: VecDeque::with_capacity(60),
name,
net_rx: ByteStats::new(0),
net_tx: ByteStats::new(0),
state,
status,
}
}
/// Find the max value in the last 30 items in the cpu stats vec
fn max_cpu_stats(&self) -> CpuStats {
match self.cpu_stats.iter().max() {
Some(value) => value.to_owned(),
None => CpuStats::new(0.0),
}
}
/// Find the max value in the last 30 items in the mem stats vec
fn max_mem_stats(&self) -> ByteStats {
match self.mem_stats.iter().max() {
Some(value) => value.to_owned(),
None => ByteStats::new(0),
}
}
/// Convert cpu stats into a vec for the charts function
fn get_cpu_dataset(&self) -> Vec<(f64, f64)> {
self.cpu_stats
.iter()
.enumerate()
.map(|i| (i.0 as f64, i.1.value))
.collect::<Vec<_>>()
}
/// Convert mem stats into a vec for the charts function
fn get_mem_dataset(&self) -> Vec<(f64, f64)> {
self.mem_stats
.iter()
.enumerate()
.map(|i| (i.0 as f64, i.1.value as f64))
.collect::<Vec<_>>()
}
/// Get all cpu chart data
fn get_cpu_chart_data(&self) -> CpuTuple {
(
self.get_cpu_dataset(),
self.max_cpu_stats(),
self.state.clone(),
)
}
/// Get all mem chart data
fn get_mem_chart_data(&self) -> MemTuple {
(
self.get_mem_dataset(),
self.max_mem_stats(),
self.state.clone(),
)
}
/// Get chart info for cpu & memory in one function
/// So only need to call .lock() once
pub fn get_chart_data(&self) -> (CpuTuple, MemTuple) {
(self.get_cpu_chart_data(), self.get_mem_chart_data())
}
}
/// Container information panel headings + widths, for nice pretty formatting
#[derive(Debug)]
pub struct Columns {
pub cpu: (String, usize),
pub image: (String, usize),
pub name: (String, usize),
pub state: (String, usize),
pub status: (String, usize),
pub mem: (String, usize),
pub net_rx: (String, usize),
pub net_tx: (String, usize),
}
impl Columns {
pub fn new() -> Self {
Self {
// 7 to allow for 100.00%
cpu: (String::from("cpu"), 7),
image: (String::from("image"), 5),
name: (String::from("name"), 4),
state: (String::from("state"), 11),
status: (String::from("status"), 16),
mem: (String::from("mem/limit"), 9),
net_rx: (String::from("↓ rx"), 5),
net_tx: (String::from("↑ tx"), 5),
}
}
}
+397
View File
@@ -0,0 +1,397 @@
use bollard::models::ContainerSummary;
use std::time::{SystemTime, UNIX_EPOCH};
use tui::widgets::ListItem;
mod container_state;
use crate::{app_error::AppError, parse_args::CliArgs, ui::log_sanitizer};
pub use container_state::*;
/// Global app_state, stored in an Arc<Mutex>
#[derive(Debug)]
pub struct AppData {
args: CliArgs,
error: Option<AppError>,
logs_parsed: bool,
pub containers: StatefulList<ContainerItem>,
pub init: bool,
pub show_error: bool,
}
impl AppData {
/// Generate a default app_state
pub fn default(args: CliArgs) -> Self {
Self {
args,
containers: StatefulList::new(vec![]),
error: None,
init: false,
logs_parsed: false,
show_error: false,
}
}
// Current time as unix timestamp
fn get_systemtime(&self) -> u64 {
SystemTime::now()
.duration_since(UNIX_EPOCH)
.expect("In our known reality, this error should never occur")
.as_secs()
}
/// Get the current select docker command
/// So know which command to execute
pub fn get_docker_command(&self) -> Option<DockerControls> {
let mut output = None;
if let Some(index) = self.containers.state.selected() {
if let Some(control_index) = self.containers.items[index]
.docker_controls
.state
.selected()
{
output =
Some(self.containers.items[index].docker_controls.items[control_index].clone())
}
}
output
}
/// Change selected choice of docker commands of selected container
pub fn docker_command_next(&mut self) {
if let Some(index) = self.containers.state.selected() {
self.containers.items[index].docker_controls.next()
}
}
/// Change selected choice of docker commands of selected container
pub fn docker_command_previous(&mut self) {
if let Some(index) = self.containers.state.selected() {
self.containers.items[index].docker_controls.previous()
}
}
/// Change selected choice of docker commands of selected container
pub fn docker_command_start(&mut self) {
if let Some(index) = self.containers.state.selected() {
self.containers.items[index].docker_controls.start()
}
}
/// Change selected choice of docker commands of selected container
pub fn docker_command_end(&mut self) {
if let Some(index) = self.containers.state.selected() {
self.containers.items[index].docker_controls.end()
}
}
/// return single app_state error
pub fn get_error(&self) -> Option<AppError> {
self.error.clone()
}
/// remove single app_state error
pub fn remove_error(&mut self) {
self.error = None;
}
/// insert single app_state error
pub fn set_error(&mut self, error: AppError) {
self.error = Some(error);
}
/// Find the if of the currently selected container
/// If any containers on system, will always return
/// Only returns None when no containers found
pub fn get_selected_container_id(&self) -> Option<String> {
let mut output = None;
if let Some(index) = self.containers.state.selected() {
let id = self
.containers
.items
.iter()
.skip(index)
.take(1)
.map(|i| i.id.to_owned())
.collect::<String>();
output = Some(id)
}
output
}
/// Find the index of the currently selected single log line
pub fn get_selected_log_index(&self) -> Option<usize> {
let mut output = None;
if let Some(id) = self.get_selected_container_id() {
if let Some(index) = self.containers.items.iter().position(|i| i.id == id) {
output = Some(index);
}
}
output
}
/// Get the title for log panel for selected container
/// will be "logs x/x"
pub fn get_log_title(&self) -> String {
if let Some(index) = self.get_selected_log_index() {
self.containers.items[index].logs.get_state_title()
} else {
String::from("")
}
}
/// select next selected log line
pub fn log_next(&mut self) {
if let Some(index) = self.get_selected_log_index() {
self.containers.items[index].logs.next()
}
}
/// select previous selected log line
pub fn log_previous(&mut self) {
if let Some(index) = self.get_selected_log_index() {
self.containers.items[index].logs.previous()
}
}
/// select last selected log line
pub fn log_end(&mut self) {
if let Some(index) = self.get_selected_log_index() {
self.containers.items[index].logs.end()
}
}
/// select first selected log line
pub fn log_start(&mut self) {
if let Some(index) = self.get_selected_log_index() {
self.containers.items[index].logs.start()
}
}
pub fn initialised(&mut self, all_ids: &[(bool, String)]) -> bool {
let count_is_running = all_ids.iter().filter(|i| i.0).count();
let number_with_cpu_status = self
.containers
.items
.iter()
.filter(|i| !i.cpu_stats.is_empty())
.count();
self.logs_parsed && count_is_running == number_with_cpu_status
}
/// Just get the total number of containers
pub fn get_container_len(&self) -> usize {
self.containers.items.len()
}
/// Find the widths for the strings in the containers panel
/// So can display nicely and evenly
pub fn get_width(&self) -> Columns {
let mut output = Columns::new();
let count = |x: &String| x.chars().count();
for container in self.containers.items.iter() {
let cpu_count = count(
&container
.cpu_stats
.back()
.unwrap_or(&CpuStats::new(0.0))
.to_string(),
);
let mem_count = count(&format!(
"{} / {}",
container.mem_stats.back().unwrap_or(&ByteStats::new(0)),
container.mem_limit
));
let net_rx_count = count(&container.net_rx.to_string());
let net_tx_count = count(&container.net_tx.to_string());
let image_count = count(&container.image);
let name_count = count(&container.name);
let state_count = count(&container.state.to_string());
let status_count = count(&container.status);
if cpu_count > output.cpu.1 {
output.cpu.1 = cpu_count;
};
if image_count > output.image.1 {
output.image.1 = image_count;
};
if mem_count > output.mem.1 {
output.mem.1 = mem_count;
};
if name_count > output.name.1 {
output.name.1 = name_count;
};
if state_count > output.state.1 {
output.state.1 = state_count;
};
if status_count > output.status.1 {
output.status.1 = status_count;
};
if net_rx_count > output.net_rx.1 {
output.net_rx.1 = net_rx_count;
};
if net_tx_count > output.net_tx.1 {
output.net_tx.1 = net_tx_count;
};
}
output
}
/// Get all containers ids
pub fn get_all_ids(&self) -> Vec<String> {
self.containers
.items
.iter()
.map(|i| i.id.to_owned())
.collect::<Vec<_>>()
}
/// find container given id
fn get_container_by_id(&mut self, id: &str) -> Option<&mut ContainerItem> {
self.containers.items.iter_mut().find(|i| i.id == id)
}
/// Update container mem + cpu stats, in single function so only need to call .lock() once
pub fn update_stats(
&mut self,
id: String,
cpu_stat: Option<f64>,
mem_stat: Option<u64>,
mem_limit: u64,
rx: u64,
tx: u64,
) {
if let Some(container) = self.get_container_by_id(&id) {
if container.cpu_stats.len() >= 60 {
container.cpu_stats.pop_front();
}
if container.mem_stats.len() >= 60 {
container.mem_stats.pop_front();
}
if let Some(cpu) = cpu_stat {
container.cpu_stats.push_back(CpuStats::new(cpu));
}
if let Some(mem) = mem_stat {
container.mem_stats.push_back(ByteStats::new(mem));
}
container.net_rx.update(rx);
container.net_tx.update(tx);
container.mem_limit.update(mem_limit);
}
}
/// Update, or insert, containers
pub fn update_containers(&mut self, containers: &[ContainerSummary]) {
let all_ids = self.get_all_ids();
if !containers.is_empty() && self.containers.state.selected().is_none() {
self.containers.start();
}
for (index, id) in all_ids.iter().enumerate() {
if !containers
.iter()
.map(|i| i.id.as_ref().unwrap())
.any(|x| x == id)
{
// If removed container is currently selected, then change selected to previous
// This will default to 0 in any edge cases
if self.containers.state.selected().is_some() {
self.containers.previous();
}
self.containers.items.remove(index);
}
}
for i in containers.iter() {
let id = i.id.as_ref().unwrap().to_owned();
let mut name = i
.names
.as_ref()
.unwrap_or(&vec!["".to_owned()])
.get(0)
.unwrap()
.to_owned();
if let Some(c) = name.chars().next() {
if c == '/' {
name.remove(0);
}
}
let state = State::from(i.state.as_ref().unwrap_or(&"dead".to_owned()).trim());
let status = i
.status
.as_ref()
.unwrap_or(&"".to_owned())
.trim()
.to_owned();
let image = i.image.as_ref().unwrap_or(&"".to_owned()).trim().to_owned();
if let Some(current_container) = self.get_container_by_id(&id) {
if current_container.name != name {
current_container.name = name
};
if current_container.status != status {
current_container.status = status
};
if current_container.state != state {
current_container.docker_controls.items = DockerControls::gen_vec(&state);
// Update the list state, needs to be None if the gen_vec returns an empty vec
match state {
State::Removing | State::Restarting | State::Unknown => {
current_container.docker_controls.state.select(None)
}
_ => current_container.docker_controls.start(),
};
current_container.state = state;
};
if current_container.image != image {
current_container.image = image
};
} else {
let mut container = ContainerItem::new(id, status, image, state, name);
container.logs.end();
self.containers.items.push(container);
}
}
}
/// update logs of a given container, based on index not id
pub fn update_log_by_index(&mut self, output: Vec<String>, index: usize) {
let tz = self.get_systemtime();
if let Some(container) = self.containers.items.get_mut(index) {
container.last_updated = tz;
let current_len = container.logs.items.len();
output.iter().for_each(|i| {
let lines = if self.args.color {
log_sanitizer::colorize_logs(i.to_owned())
} else if self.args.raw {
log_sanitizer::raw(i.to_owned())
} else {
log_sanitizer::remove_ansi(i.to_owned())
};
container.logs.items.push(ListItem::new(lines));
});
if container.logs.state.selected().is_none()
|| container.logs.state.selected().unwrap() + 1 == current_len
{
container.logs.end();
}
}
self.logs_parsed = true;
}
pub fn update_all_logs(&mut self, all_logs: Vec<Vec<String>>) {
for (index, output) in all_logs.into_iter().enumerate() {
self.update_log_by_index(output, index);
}
}
}