use crate::cluster::Cluster;
use crate::command::{Command, MainCommand, UtilityCommands};
use crate::theme::init_themes_file;
use crate::version::VERSION_MESSAGE;
use app::configuration::{
ClusterConfig, GlobalConfig, SchemaRegistryConfig, Workspace, YozefuConfig,
};
use app::{APPLICATION_NAME, BINARY_NAME};
use lib::Error;
use reqwest::Url;
use std::collections::HashMap;
use std::fs;
use std::path::PathBuf;
use tui::error::TuiError;
pub use clap::Parser;
use indexmap::IndexMap;
#[derive(Parser)]
#[command(author,
version = VERSION_MESSAGE,
about = "A terminal user interface to navigate Kafka topics and search for Kafka records.",
name = APPLICATION_NAME,
bin_name = BINARY_NAME,
display_name = APPLICATION_NAME,
long_about = None,
propagate_version = true,
args_conflicts_with_subcommands = true
)]
pub struct Cli<T>
where
T: Cluster,
{
#[command(subcommand)]
subcommands: Option<UtilityCommands>,
#[command(flatten)]
default_command: MainCommand<T>,
#[clap(skip)]
logs_file: Option<PathBuf>,
}
impl<T> Cli<T>
where
T: Cluster,
{
pub async fn execute(&self) -> Result<(), TuiError> {
self.run(None).await
}
pub async fn execute_with(&self, yozefu_config: YozefuConfig) -> Result<(), TuiError> {
self.run(Some(yozefu_config)).await
}
pub fn cluster(&self) -> Option<T> {
match self.subcommands.is_some() {
true => None,
false => Some(self.default_command.cluster()),
}
}
pub fn is_main_command(&self) -> bool {
self.cluster().is_some()
}
pub fn logs_file(&mut self, logs: PathBuf) -> &mut Self {
self.logs_file = Some(logs);
self
}
async fn run(&self, yozefu_config: Option<YozefuConfig>) -> Result<(), TuiError> {
self.init_files().await?;
match &self.subcommands {
Some(c) => c.execute().await.map_err(std::convert::Into::into),
None => {
let yozefu_config = match yozefu_config {
None => self.default_command.yozefu_config()?,
Some(c) => c,
};
let mut command = self.default_command.clone();
command.logs_file.clone_from(&self.logs_file);
command.execute(yozefu_config).await
}
}
}
async fn init_files(&self) -> Result<(), Error> {
let workspace = match &self.subcommands {
Some(UtilityCommands::Config(c)) => &c.global,
_ => &self.default_command.global,
}
.workspace();
init_config_file(&workspace)?;
init_themes_file(&workspace).await?;
Ok(())
}
}
fn init_config_file(workspace: &Workspace) -> Result<PathBuf, Error> {
let path = workspace.config_file();
if fs::metadata(&path).is_ok() {
return Ok(path);
}
let mut config = GlobalConfig::new(&path);
let mut localhost_config = IndexMap::new();
localhost_config.insert(
"bootstrap.servers".to_string(),
"localhost:9092".to_string(),
);
localhost_config.insert("security.protocol".to_string(), "plaintext".to_string());
localhost_config.insert("broker.address.family".to_string(), "v4".to_string());
config
.default_kafka_config
.insert("fetch.min.bytes".to_string(), "10000".to_string());
config.clusters.insert(
"localhost".into(),
ClusterConfig {
kafka: localhost_config,
schema_registry: Some(SchemaRegistryConfig {
url: Url::parse("http://localhost:8081").unwrap(),
headers: HashMap::default(),
}),
..Default::default()
},
);
if let Some(parent) = path.parent() {
fs::create_dir_all(parent).map_err(|e| {
std::io::Error::new(
e.kind(),
format!("Failed to create the configuration directory '{}': {}. Check you have write permissions.", parent.display(), e)
)
})?;
fs::create_dir_all(workspace.filters_dir()).map_err(|e| {
std::io::Error::new(
e.kind(),
format!("Failed to create the filters directory '{}': {}. Check you have write permissions.", workspace.filters_dir().display(), e)
)
})?;
}
fs::write(&path, serde_json::to_string_pretty(&config).unwrap())
.map_err(|e| {
std::io::Error::new(
e.kind(),
format!("Failed to initialize the configuration file '{}': {}. Check you have write permissions.", path.display(), e)
)
})?;
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
let mut perms: fs::Permissions = fs::metadata(&path)?.permissions();
perms.set_mode(0o600);
fs::set_permissions(&path, perms)?;
}
Ok(path)
}
#[test]
pub fn test_conflicts() {
use clap::CommandFactory;
Cli::<String>::command().debug_assert();
}
#[test]
fn test_valid_themes() {
use std::collections::HashMap;
use tui::Theme;
let content = include_str!("../themes.json");
let themes: HashMap<String, Theme> = serde_json::from_str(content).unwrap();
assert!(themes.keys().len() >= 3)
}
#[test]
fn initialize_config_file_on_readonly_root_partition() {
let workspace = Workspace::new(
&PathBuf::from("/tmp/yozefu-readonly"),
GlobalConfig::new(&PathBuf::from("/tmp/yozefu-readonly/config.json")),
PathBuf::from("/tmp/yozefu-readonly").join(Workspace::LOGS_FILENAME),
);
let directory = &workspace.path;
let _ = fs::remove_dir_all(directory);
fs::create_dir_all(directory).unwrap();
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
let mut perms: fs::Permissions = fs::metadata(directory).unwrap().permissions();
perms.set_mode(0o000);
let _ = fs::set_permissions(directory, perms);
let result = init_config_file(&workspace);
assert!(result.is_err());
}
}