use clap::ValueEnum;
use rayon::prelude::*;
use serde::{Deserialize, Serialize};
use std::collections::BTreeMap;
use std::error::Error;
pub mod application;
pub mod container;
pub mod docker;
pub mod end_of_life;
pub type ContainerMap = BTreeMap<String, container::Container>;
pub type ApplicationMap = BTreeMap<String, application::Application>;
#[derive(ValueEnum, Serialize, Deserialize, Clone, Debug, Default)]
pub enum FilterFunction {
#[default]
Any,
All,
}
#[derive(Serialize, Deserialize, Debug, Default)]
pub struct Options {
clean_after_query: bool,
tags: Option<Vec<String>>,
filter_function: FilterFunction,
names: Option<Vec<String>>,
}
impl Options {
pub fn new(
clean_after_query: bool,
tags: Option<Vec<String>>,
names: Option<Vec<String>>,
filter_function: FilterFunction,
) -> Self {
Self {
clean_after_query,
tags,
names,
filter_function,
}
}
}
#[derive(Serialize, Deserialize, Default)]
pub struct Config {
containers: ContainerMap,
applications: ApplicationMap,
options: Options,
}
impl Config {
pub fn new(containers: ContainerMap, applications: ApplicationMap, options: Options) -> Self {
let containers = Self::filter_by_names(containers, &options.names);
let containers = Self::filter_by_tags(containers, &options.tags, &options.filter_function);
Self {
containers,
applications,
options,
}
}
pub fn run(&self) -> Result<Vec<container::Status>, Box<dyn Error>> {
Ok(self
.containers
.par_iter()
.map(|(name, container)| self.container_status(name, container))
.collect())
}
fn filter_by_tags(
containers: ContainerMap,
tags: &Option<Vec<String>>,
filter_function: &FilterFunction,
) -> ContainerMap {
match tags {
Some(x) => containers
.into_iter()
.filter(|(_, c)| match &c.tags {
Some(t) => match &filter_function {
FilterFunction::Any => t.iter().any(|y| x.contains(y)),
FilterFunction::All => x.iter().all(|y| t.contains(y)),
},
None => false,
})
.collect(),
None => containers,
}
}
fn filter_by_names(containers: ContainerMap, names: &Option<Vec<String>>) -> ContainerMap {
match names {
Some(x) => containers
.into_iter()
.filter(|(name, _)| x.contains(name))
.collect(),
None => containers,
}
}
fn container_status(
&self,
name: &String,
container: &container::Container,
) -> container::Status {
let mut container_status = container::Status::new(name.clone());
let mut apps = container.apps.clone();
apps.sort();
let instance = docker::Docker::new(name, &container.path);
instance.run().expect("Unable to start docker container");
for app_name in apps {
let app = match self.applications.get(&app_name) {
Some(app) => app,
None => {
eprintln!(
"Config error for {} on {}: App is not defined",
&app_name, &name
);
eprintln!(
"-- hint: If you're sure you have a config for {}, look for typos.",
&app_name
);
continue;
}
};
let output = instance.execute(&app.version_command);
match app.query_version(&output) {
Ok(version) => {
let eol_status: Option<String> = match &app.eol {
Some(x) => match x.query(&version) {
Ok(cycle) => Some(cycle.into()),
_ => None,
},
_ => None,
};
container_status.apps.push(application::Status {
name: app_name,
version,
eol_status,
});
}
_ => {
eprintln!("Error querying app version for {} on {}", &app_name, &name);
eprintln!("-- hint: Your version command was: {}", app.version_command);
eprintln!(
" Your regex query was: {}",
app.version_regex.as_str()
);
eprintln!(" Your regex input was: {output}");
}
}
}
instance
.stop(self.options.clean_after_query)
.expect("Unable to clean up docker container");
container_status
}
}
#[cfg(test)]
mod tests {
use crate::{
container::Container, ApplicationMap, Config, ContainerMap, FilterFunction, Options,
};
#[test]
fn filter_by_any() {
let containers = ContainerMap::from([
(
String::from("test"),
Container {
tags: Some(vec![String::from("one"), String::from("two")]),
..Default::default()
},
),
(
String::from("again"),
Container {
tags: None,
..Default::default()
},
),
]);
let applications = ApplicationMap::new();
let options = Options {
tags: Some(vec![String::from("one")]),
filter_function: FilterFunction::Any,
..Default::default()
};
let config = Config::new(containers, applications, options);
assert_eq!(config.containers.len(), 1)
}
#[test]
fn filter_by_all() {
let containers = ContainerMap::from([
(
String::from("test"),
Container {
tags: Some(vec![String::from("one"), String::from("two")]),
..Default::default()
},
),
(
String::from("again"),
Container {
tags: Some(vec![String::from("one")]),
..Default::default()
},
),
]);
let applications = ApplicationMap::new();
let options = Options {
tags: Some(vec![String::from("one"), String::from("two")]),
filter_function: FilterFunction::All,
..Default::default()
};
let config = Config::new(containers, applications, options);
assert_eq!(config.containers.len(), 1)
}
#[test]
fn filter_by_name() {
let containers = ContainerMap::from([
(String::from("test"), Container::default()),
(String::from("again"), Container::default()),
]);
let applications = ApplicationMap::new();
let options = Options {
names: Some(vec![String::from("test")]),
..Default::default()
};
let config = Config::new(containers, applications, options);
assert_eq!(config.containers.len(), 1);
}
}