use clap::Parser;
use log::{debug, trace};
use pingora_error::{Error, ErrorType::*, OrErr, Result};
use serde::{Deserialize, Serialize};
use std::ffi::OsString;
use std::fs;
const DEFAULT_MAX_RETRIES: usize = 16;
#[derive(Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(default)]
pub struct ServerConf {
pub version: usize,
pub daemon: bool,
pub error_log: Option<String>,
pub pid_file: String,
pub upgrade_sock: String,
pub user: Option<String>,
pub group: Option<String>,
pub threads: usize,
pub listener_tasks_per_fd: usize,
pub work_stealing: bool,
pub ca_file: Option<String>,
#[cfg(feature = "s2n")]
pub s2n_config_cache_size: Option<usize>,
pub grace_period_seconds: Option<u64>,
pub graceful_shutdown_timeout_seconds: Option<u64>,
pub client_bind_to_ipv4: Vec<String>,
pub client_bind_to_ipv6: Vec<String>,
pub upstream_keepalive_pool_size: usize,
pub upstream_connect_offload_threadpools: Option<usize>,
pub upstream_connect_offload_thread_per_pool: Option<usize>,
pub upstream_debug_ssl_keylog: bool,
pub max_retries: usize,
pub upgrade_sock_connect_accept_max_retries: Option<usize>,
}
impl Default for ServerConf {
fn default() -> Self {
ServerConf {
version: 0,
client_bind_to_ipv4: vec![],
client_bind_to_ipv6: vec![],
ca_file: None,
#[cfg(feature = "s2n")]
s2n_config_cache_size: None,
daemon: false,
error_log: None,
upstream_debug_ssl_keylog: false,
pid_file: "/tmp/pingora.pid".to_string(),
upgrade_sock: "/tmp/pingora_upgrade.sock".to_string(),
user: None,
group: None,
threads: 1,
listener_tasks_per_fd: 1,
work_stealing: true,
upstream_keepalive_pool_size: 128,
upstream_connect_offload_threadpools: None,
upstream_connect_offload_thread_per_pool: None,
grace_period_seconds: None,
graceful_shutdown_timeout_seconds: None,
max_retries: DEFAULT_MAX_RETRIES,
upgrade_sock_connect_accept_max_retries: None,
}
}
}
#[derive(Parser, Debug, Default)]
#[clap(name = "basic", long_about = None)]
pub struct Opt {
#[clap(
short,
long,
help = "This is the base set of command line arguments for a pingora-based service",
long_help = None
)]
pub upgrade: bool,
#[clap(short, long)]
pub daemon: bool,
#[clap(long, hide = true)]
pub nocapture: bool,
#[clap(
short,
long,
help = "This flag is useful for upgrading service where the user wants \
to make sure the new service can start before shutting down \
the old server process.",
long_help = None
)]
pub test: bool,
#[clap(short, long, help = "The path to the configuration file.", long_help = None)]
pub conf: Option<String>,
}
impl ServerConf {
pub fn load_from_yaml<P>(path: P) -> Result<Self>
where
P: AsRef<std::path::Path> + std::fmt::Display,
{
let conf_str = fs::read_to_string(&path).or_err_with(ReadError, || {
format!("Unable to read conf file from {path}")
})?;
debug!("Conf file read from {path}");
Self::from_yaml(&conf_str)
}
pub fn load_yaml_with_opt_override(opt: &Opt) -> Result<Self> {
if let Some(path) = &opt.conf {
let mut conf = Self::load_from_yaml(path)?;
conf.merge_with_opt(opt);
Ok(conf)
} else {
Error::e_explain(ReadError, "No path specified")
}
}
pub fn new() -> Option<Self> {
Self::from_yaml("---\nversion: 1").ok()
}
pub fn new_with_opt_override(opt: &Opt) -> Option<Self> {
let conf = Self::new();
match conf {
Some(mut c) => {
c.merge_with_opt(opt);
Some(c)
}
None => None,
}
}
pub fn from_yaml(conf_str: &str) -> Result<Self> {
trace!("Read conf file: {conf_str}");
let conf: ServerConf = serde_yaml::from_str(conf_str).or_err_with(ReadError, || {
format!("Unable to parse yaml conf {conf_str}")
})?;
trace!("Loaded conf: {conf:?}");
conf.validate()
}
pub fn to_yaml(&self) -> String {
serde_yaml::to_string(self).unwrap()
}
pub fn validate(self) -> Result<Self> {
Ok(self)
}
pub fn merge_with_opt(&mut self, opt: &Opt) {
if opt.daemon {
self.daemon = true;
}
}
}
impl Opt {
pub fn parse_args() -> Self {
Opt::parse()
}
pub fn parse_from_args<I, T>(args: I) -> Self
where
I: IntoIterator<Item = T>,
T: Into<OsString> + Clone,
{
Opt::parse_from(args)
}
}
#[cfg(test)]
mod tests {
use super::*;
fn init_log() {
let _ = env_logger::builder().is_test(true).try_init();
}
#[test]
fn not_a_test_i_cannot_write_yaml_by_hand() {
init_log();
let conf = ServerConf {
version: 1,
client_bind_to_ipv4: vec!["1.2.3.4".to_string(), "5.6.7.8".to_string()],
client_bind_to_ipv6: vec![],
ca_file: None,
#[cfg(feature = "s2n")]
s2n_config_cache_size: None,
daemon: false,
error_log: None,
upstream_debug_ssl_keylog: false,
pid_file: "".to_string(),
upgrade_sock: "".to_string(),
user: None,
group: None,
threads: 1,
listener_tasks_per_fd: 1,
work_stealing: true,
upstream_keepalive_pool_size: 4,
upstream_connect_offload_threadpools: None,
upstream_connect_offload_thread_per_pool: None,
grace_period_seconds: None,
graceful_shutdown_timeout_seconds: None,
max_retries: 1,
upgrade_sock_connect_accept_max_retries: None,
};
println!("{}", conf.to_yaml());
}
#[test]
fn test_load_file() {
init_log();
let conf_str = r#"
---
version: 1
client_bind_to_ipv4:
- 1.2.3.4
- 5.6.7.8
client_bind_to_ipv6: []
"#
.to_string();
let conf = ServerConf::from_yaml(&conf_str).unwrap();
assert_eq!(2, conf.client_bind_to_ipv4.len());
assert_eq!(0, conf.client_bind_to_ipv6.len());
assert_eq!(1, conf.version);
}
#[test]
fn test_default() {
init_log();
let conf_str = r#"
---
version: 1
"#
.to_string();
let conf = ServerConf::from_yaml(&conf_str).unwrap();
assert_eq!(0, conf.client_bind_to_ipv4.len());
assert_eq!(0, conf.client_bind_to_ipv6.len());
assert_eq!(1, conf.version);
assert_eq!(DEFAULT_MAX_RETRIES, conf.max_retries);
assert_eq!("/tmp/pingora.pid", conf.pid_file);
}
}