You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
distant/src/options/config.rs

308 lines
10 KiB
Rust

use std::path::PathBuf;
use anyhow::Context;
use once_cell::sync::Lazy;
use serde::{Deserialize, Serialize};
use super::common;
use crate::constants;
mod client;
mod generate;
mod manager;
mod server;
pub use client::*;
pub use generate::*;
pub use manager::*;
pub use server::*;
const DEFAULT_RAW_STR: &str = include_str!("config.toml");
/// Represents configuration settings for all of distant
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub struct Config {
pub client: ClientConfig,
pub generate: GenerateConfig,
pub manager: ManagerConfig,
pub server: ServerConfig,
}
impl Config {
/// Returns a reference to the default config file as a raw str.
pub const fn default_raw_str() -> &'static str {
DEFAULT_RAW_STR
}
/// Loads the configuration from multiple sources in a blocking fashion
///
/// 1. If `custom` is provided, it is used by itself as the source for configuration
/// 2. Otherwise, if `custom` is not provided, will attempt to load from global and user
/// config files, merging together if they both exist
/// 3. Otherwise if no `custom` path and none of the standard configuration paths exist,
/// then the default configuration is returned instead
pub fn load_multi(custom: Option<PathBuf>) -> anyhow::Result<Self> {
match custom {
Some(path) => {
toml_edit::de::from_slice(&std::fs::read(path)?).context("Failed to parse config")
}
None => {
let paths = vec![
constants::global::CONFIG_FILE_PATH.as_path(),
constants::user::CONFIG_FILE_PATH.as_path(),
];
match (paths[0].exists(), paths[1].exists()) {
// At least one standard path exists, so load it
(exists_1, exists_2) if exists_1 || exists_2 => {
use config::{Config, File};
let config = Config::builder()
.add_source(File::from(paths[0]).required(exists_1))
.add_source(File::from(paths[1]).required(exists_2))
.build()
.context("Failed to build config from paths")?;
config.try_deserialize().context("Failed to parse config")
}
// None of our standard paths exist, so use the default value instead
_ => Ok(Self::default()),
}
}
}
}
/// Loads the specified `path` as a [`Config`]
#[cfg(test)]
pub async fn load(path: impl AsRef<std::path::Path>) -> anyhow::Result<Self> {
let bytes = tokio::fs::read(path.as_ref())
.await
.with_context(|| format!("Failed to read config file {:?}", path.as_ref()))?;
toml_edit::de::from_slice(&bytes).context("Failed to parse config")
}
}
impl Default for Config {
fn default() -> Self {
static DEFAULT_CONFIG: Lazy<Config> = Lazy::new(|| {
toml_edit::de::from_str(Config::default_raw_str())
.expect("Default config failed to parse")
});
DEFAULT_CONFIG.clone()
}
}
#[cfg(test)]
mod tests {
use std::net::Ipv4Addr;
use std::time::Duration;
use common::*;
use distant_core::net::common::{Host, Map, PortRange};
use distant_core::net::map;
use distant_core::net::server::Shutdown;
use test_log::test;
use super::*;
#[test]
fn default_should_parse_config_from_internal_toml() {
let config = Config::default();
assert_eq!(
config,
Config {
client: ClientConfig {
api: ClientApiConfig {
timeout: Some(Seconds::from(0u32))
},
connect: ClientConnectConfig {
options: Map::new()
},
launch: ClientLaunchConfig {
distant: ClientLaunchDistantConfig {
bin: Some("distant".to_owned()),
bind_server: Some(BindAddress::Ssh),
args: Some("".to_string())
},
options: Map::new(),
},
logging: LoggingSettings {
log_level: Some(LogLevel::Info),
log_file: None
},
network: NetworkSettings {
unix_socket: None,
windows_pipe: None
},
},
generate: GenerateConfig {
logging: LoggingSettings {
log_level: Some(LogLevel::Info),
log_file: None
},
},
manager: ManagerConfig {
access: Some(AccessControl::Owner),
logging: LoggingSettings {
log_level: Some(LogLevel::Info),
log_file: None
},
network: NetworkSettings {
unix_socket: None,
windows_pipe: None
},
},
server: ServerConfig {
listen: ServerListenConfig {
host: Some(BindAddress::Any),
port: Some(0.into()),
use_ipv6: false,
shutdown: Some(Shutdown::Never),
current_dir: None,
},
logging: LoggingSettings {
log_level: Some(LogLevel::Info),
log_file: None
},
watch: ServerWatchConfig {
native: true,
poll_interval: None,
compare_contents: false,
debounce_timeout: None,
debounce_tick_rate: None,
},
},
}
);
}
#[test(tokio::test)]
async fn load_should_parse_config_from_specified_file() {
use assert_fs::prelude::*;
let config_file = assert_fs::NamedTempFile::new("config.toml").unwrap();
config_file
.write_str(
r#"
[client]
log_file = "client-log-file"
log_level = "trace"
unix_socket = "client-unix-socket"
windows_pipe = "client-windows-pipe"
[client.api]
timeout = 456
[client.connect]
options = "key=\"value\",key2=\"value2\""
[client.launch]
bin = "some-bin"
bind_server = "any"
args = "a b c"
options = "key3=\"value3\",key4=\"value4\""
[generate]
log_file = "generate-log-file"
log_level = "debug"
[manager]
log_file = "manager-log-file"
log_level = "warn"
access = "anyone"
unix_socket = "manager-unix-socket"
windows_pipe = "manager-windows-pipe"
[server]
log_file = "server-log-file"
log_level = "error"
[server.listen]
host = "127.0.0.1"
port = "8080:8089"
use_ipv6 = true
shutdown = "after=123"
current_dir = "server-current-dir"
[server.watch]
native = false
poll_interval = 12.5
compare_contents = true
debounce_timeout = 10.5
debounce_tick_rate = 0.678
"#,
)
.unwrap();
let config = Config::load(config_file.path()).await.unwrap();
assert_eq!(
config,
Config {
client: ClientConfig {
api: ClientApiConfig {
timeout: Some(Seconds::from(456u32))
},
connect: ClientConnectConfig {
options: map!("key" -> "value", "key2" -> "value2"),
},
launch: ClientLaunchConfig {
distant: ClientLaunchDistantConfig {
bin: Some("some-bin".to_owned()),
bind_server: Some(BindAddress::Any),
args: Some(String::from("a b c"))
},
options: map!("key3" -> "value3", "key4" -> "value4"),
},
logging: LoggingSettings {
log_level: Some(LogLevel::Trace),
log_file: Some(PathBuf::from("client-log-file")),
},
network: NetworkSettings {
unix_socket: Some(PathBuf::from("client-unix-socket")),
windows_pipe: Some(String::from("client-windows-pipe"))
},
},
generate: GenerateConfig {
logging: LoggingSettings {
log_level: Some(LogLevel::Debug),
log_file: Some(PathBuf::from("generate-log-file"))
},
},
manager: ManagerConfig {
access: Some(AccessControl::Anyone),
logging: LoggingSettings {
log_level: Some(LogLevel::Warn),
log_file: Some(PathBuf::from("manager-log-file"))
},
network: NetworkSettings {
unix_socket: Some(PathBuf::from("manager-unix-socket")),
windows_pipe: Some(String::from("manager-windows-pipe")),
},
},
server: ServerConfig {
listen: ServerListenConfig {
host: Some(BindAddress::Host(Host::Ipv4(Ipv4Addr::new(127, 0, 0, 1)))),
port: Some(PortRange {
start: 8080,
end: Some(8089)
}),
use_ipv6: true,
shutdown: Some(Shutdown::After(Duration::from_secs(123))),
current_dir: Some(PathBuf::from("server-current-dir")),
},
logging: LoggingSettings {
log_level: Some(LogLevel::Error),
log_file: Some(PathBuf::from("server-log-file")),
},
watch: ServerWatchConfig {
native: false,
poll_interval: Some(Seconds::try_from(12.5).unwrap()),
compare_contents: true,
debounce_timeout: Some(Seconds::try_from(10.5).unwrap()),
debounce_tick_rate: Some(Seconds::try_from(0.678).unwrap())
},
},
}
);
}
}