New configuration parsing using envy. Added minimal functionnality for

the future ACME parameters. Tests written and passing.

WIP: added envy dependncy and ConfigOpts structs that will constitute Diplonat's configuration

WIP: ConfigOpts from_env() and validate() methods written. No API change (the env names remain unchanged)! Now need to use our new ConfigOpts struct instead of Environment, and update references to the environment variables in the code.

WIP: RuntimeConfig with business logic done. Tests written, but they are all running from the same process - setting environment variables in each test produces incoherent results. Another solution for testing is needed.

WIP: tests are fully written using 'from_iter' and all passing
This commit is contained in:
LUXEY Adrien 2021-08-14 19:12:18 +02:00
parent ade0090cdb
commit ae9550ce23
7 changed files with 340 additions and 11 deletions

10
Cargo.lock generated
View File

@ -140,6 +140,7 @@ name = "diplonat"
version = "0.1.0"
dependencies = [
"anyhow",
"envy",
"futures",
"igd",
"iptables",
@ -181,6 +182,15 @@ dependencies = [
"termcolor",
]
[[package]]
name = "envy"
version = "0.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3f47e0157f2cb54f5ae1bd371b30a2ae4311e1c028f575cd4e81de7353215965"
dependencies = [
"serde",
]
[[package]]
name = "fnv"
version = "1.0.6"

View File

@ -7,15 +7,16 @@ edition = "2018"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
reqwest = { version = "0.10", features = ["json"] }
anyhow = "1.0.28"
envy = "0.4"
futures = "0.3.5"
igd = { version = "0.10.0", features = ["aio"] }
iptables = "0.2.2"
log = "0.4"
pretty_env_logger = "0.4"
tokio = "0.2"
futures = "0.3.5"
serde = { version = "1.0.107", features = ["derive"] }
serde_json = "1.0.53"
serde-lexpr = "0.1.1"
anyhow = "1.0.28"
iptables = "0.2.2"
regex = "1"
reqwest = { version = "0.10", features = ["json"] }
serde = { version = "1.0.107", features = ["derive"] }
serde-lexpr = "0.1.1"
serde_json = "1.0.53"
tokio = "0.2"

11
src/config/mod.rs Normal file
View File

@ -0,0 +1,11 @@
mod options;
#[cfg(test)]
mod options_test;
mod runtime;
pub use options::{ConfigOpts, ConfigOptsAcme, ConfigOptsBase, ConfigOptsConsul};
pub use runtime::{RuntimeConfig, RuntimeConfigAcme, RuntimeConfigConsul, RuntimeConfigFirewall, RuntimeConfigIgd};
pub const EXPIRATION_TIME: u16 = 300;
pub const REFRESH_TIME: u16 = 60;
pub const CONSUL_URL: &str = "http://127.0.0.1:8500";

75
src/config/options.rs Normal file
View File

@ -0,0 +1,75 @@
use anyhow::Result;
use serde::Deserialize;
use crate::config::RuntimeConfig;
// This code is inspired by the Trunk crate (https://github.com/thedodd/trunk)
// This file parses the options that can be declared in the environment.
// runtime.rs applies business logic and builds RuntimeConfig structs.
/// Base configuration options
#[derive(Clone, Debug, Default, Deserialize)]
pub struct ConfigOptsBase {
/// This node's private IP address [default: None]
pub private_ip: Option<String>,
/// Expiration time for IGD rules [default: 60]
pub expiration_time: Option<u16>,
/// Refresh time for IGD and Firewall rules [default: 300]
pub refresh_time: Option<u16>,
}
/// ACME configuration options
#[derive(Clone, Debug, Default, Deserialize)]
pub struct ConfigOptsAcme {
/// Whether ACME is enabled [default: false]
#[serde(default)]
pub enable: bool,
/// The default domain holder's e-mail [default: None]
pub email: Option<String>,
}
/// Consul configuration options
#[derive(Clone, Debug, Default, Deserialize)]
pub struct ConfigOptsConsul {
/// Consul's node name [default: None]
pub node_name: Option<String>,
/// Consul's REST URL [default: "http://127.0.0.1:8500"]
pub url: Option<String>,
}
/// Model of all potential configuration options
#[derive(Debug)]
pub struct ConfigOpts {
pub base: ConfigOptsBase,
pub acme: ConfigOptsAcme,
pub consul: ConfigOptsConsul,
}
impl ConfigOpts {
pub fn from_env() -> Result<RuntimeConfig> {
let base: ConfigOptsBase = envy::prefixed("DIPLONAT_").from_env()?;
let consul: ConfigOptsConsul = envy::prefixed("DIPLONAT_CONSUL_").from_env()?;
let acme: ConfigOptsAcme = envy::prefixed("DIPLONAT_ACME_").from_env()?;
RuntimeConfig::new(Self {
base: base,
consul: consul,
acme: acme,
})
}
pub fn from_iter<Iter: Clone>(iter: Iter) -> Result<RuntimeConfig>
where Iter: IntoIterator<Item = (String, String)> {
let base: ConfigOptsBase = envy::prefixed("DIPLONAT_").from_iter(iter.clone())?;
let consul: ConfigOptsConsul = envy::prefixed("DIPLONAT_CONSUL_").from_iter(iter.clone())?;
let acme: ConfigOptsAcme = envy::prefixed("DIPLONAT_ACME_").from_iter(iter.clone())?;
RuntimeConfig::new(Self {
base: base,
consul: consul,
acme: acme,
})
}
}

119
src/config/options_test.rs Normal file
View File

@ -0,0 +1,119 @@
use std::collections::HashMap;
use std::env;
use std::time::Duration;
use crate::config::*;
// Environment variables are set for the entire process and
// tests are run whithin the same process.
// => We cannot test ConfigOpts::from_env(),
// because tests modify each other's environment.
// This is why we only test ConfigOpts::from_iter(iter).
fn minimal_valid_options() -> HashMap<String, String> {
let mut opts = HashMap::new();
opts.insert("DIPLONAT_PRIVATE_IP".to_string(), "172.123.43.555".to_string());
opts.insert("DIPLONAT_CONSUL_NODE_NAME".to_string(), "consul_node".to_string());
opts
}
fn all_valid_options() -> HashMap<String, String> {
let mut opts = minimal_valid_options();
opts.insert("DIPLONAT_EXPIRATION_TIME".to_string(), "30".to_string());
opts.insert("DIPLONAT_REFRESH_TIME".to_string(), "10".to_string());
opts.insert("DIPLONAT_CONSUL_URL".to_string(), "http://127.0.0.1:9999".to_string());
opts.insert("DIPLONAT_ACME_ENABLE".to_string(), "true".to_string());
opts.insert("DIPLONAT_ACME_EMAIL".to_string(), "bozo@bozo.net".to_string());
opts
}
#[test]
#[should_panic]
fn err_empty_env() {
ConfigOpts::from_env();
}
#[test]
fn ok_from_iter_minimal_valid_options() {
let opts = minimal_valid_options();
let rt_config = ConfigOpts::from_iter(opts.clone()).unwrap();
assert!(rt_config.acme.is_none());
assert_eq!(
&rt_config.consul.node_name,
opts.get(&"DIPLONAT_CONSUL_NODE_NAME".to_string()).unwrap()
);
assert_eq!(
rt_config.consul.url,
CONSUL_URL.to_string()
);
assert_eq!(
rt_config.firewall.refresh_time,
Duration::from_secs(REFRESH_TIME.into())
);
assert_eq!(
&rt_config.igd.private_ip,
opts.get(&"DIPLONAT_PRIVATE_IP".to_string()).unwrap()
);
assert_eq!(
rt_config.igd.expiration_time,
Duration::from_secs(EXPIRATION_TIME.into())
);
assert_eq!(
rt_config.igd.refresh_time,
Duration::from_secs(REFRESH_TIME.into())
);
}
#[test]
#[should_panic]
fn err_from_iter_invalid_refresh_time() {
let mut opts = minimal_valid_options();
opts.insert("DIPLONAT_EXPIRATION_TIME".to_string(), "60".to_string());
opts.insert("DIPLONAT_REFRESH_TIME".to_string(), "60".to_string());
let rt_config = ConfigOpts::from_iter(opts.clone()).unwrap();
}
#[test]
fn ok_from_iter_all_valid_options() {
let opts = all_valid_options();
let rt_config = ConfigOpts::from_iter(opts.clone()).unwrap();
let expiration_time = Duration::from_secs(
opts.get(&"DIPLONAT_EXPIRATION_TIME".to_string()).unwrap()
.parse::<u64>().unwrap()
.into());
let refresh_time = Duration::from_secs(
opts.get(&"DIPLONAT_REFRESH_TIME".to_string()).unwrap()
.parse::<u64>().unwrap()
.into());
assert!(rt_config.acme.is_some());
assert_eq!(
&rt_config.acme.unwrap().email,
opts.get(&"DIPLONAT_ACME_EMAIL".to_string()).unwrap());
assert_eq!(
&rt_config.consul.node_name,
opts.get(&"DIPLONAT_CONSUL_NODE_NAME".to_string()).unwrap()
);
assert_eq!(
&rt_config.consul.url,
opts.get(&"DIPLONAT_CONSUL_URL".to_string()).unwrap()
);
assert_eq!(
rt_config.firewall.refresh_time,
refresh_time
);
assert_eq!(
&rt_config.igd.private_ip,
opts.get(&"DIPLONAT_PRIVATE_IP".to_string()).unwrap()
);
assert_eq!(
rt_config.igd.expiration_time,
expiration_time
);
assert_eq!(
rt_config.igd.refresh_time,
refresh_time
);
}

112
src/config/runtime.rs Normal file
View File

@ -0,0 +1,112 @@
use std::time::Duration;
use anyhow::{Result, anyhow};
use crate::config::{ConfigOpts, ConfigOptsAcme, ConfigOptsBase, ConfigOptsConsul};
// This code is inspired by the Trunk crate (https://github.com/thedodd/trunk)
// In this file, we take ConfigOpts and transform them into ready-to-use RuntimeConfig.
// We apply default values and business logic.
pub struct RuntimeConfigAcme {
pub email: String,
}
pub struct RuntimeConfigConsul {
pub node_name: String,
pub url: String,
}
pub struct RuntimeConfigFirewall {
pub refresh_time: Duration,
}
pub struct RuntimeConfigIgd {
pub private_ip: String,
pub expiration_time: Duration,
pub refresh_time: Duration,
}
pub struct RuntimeConfig {
pub acme: Option<RuntimeConfigAcme>,
pub consul: RuntimeConfigConsul,
pub firewall: RuntimeConfigFirewall,
pub igd: RuntimeConfigIgd,
}
impl RuntimeConfig {
pub fn new(opts: ConfigOpts) -> Result<Self> {
let acme = RuntimeConfigAcme::new(opts.acme.clone())?;
let consul = RuntimeConfigConsul::new(opts.consul.clone())?;
let firewall = RuntimeConfigFirewall::new(opts.base.clone())?;
let igd = RuntimeConfigIgd::new(opts.base.clone())?;
Ok(Self {
acme,
consul,
firewall,
igd,
})
}
}
impl RuntimeConfigAcme {
pub fn new(opts: ConfigOptsAcme) -> Result<Option<Self>> {
if !opts.enable {
return Ok(None);
}
let email = opts.email.unwrap();
Ok(Some(Self {
email,
}))
}
}
impl RuntimeConfigConsul {
pub(super) fn new(opts: ConfigOptsConsul) -> Result<Self> {
let node_name = opts.node_name.unwrap();
let url = opts.url.unwrap_or(super::CONSUL_URL.to_string());
Ok(Self {
node_name,
url,
})
}
}
impl RuntimeConfigFirewall {
pub(super) fn new(opts: ConfigOptsBase) -> Result<Self> {
let refresh_time = Duration::from_secs(
opts.refresh_time.unwrap_or(super::REFRESH_TIME).into());
Ok(Self {
refresh_time,
})
}
}
impl RuntimeConfigIgd {
pub(super) fn new(opts: ConfigOptsBase) -> Result<Self> {
let private_ip = opts.private_ip.unwrap();
let expiration_time = Duration::from_secs(
opts.expiration_time.unwrap_or(super::EXPIRATION_TIME).into());
let refresh_time = Duration::from_secs(
opts.refresh_time.unwrap_or(super::REFRESH_TIME).into());
if refresh_time.as_secs() * 2 > expiration_time.as_secs() {
return Err(anyhow!(
"IGD expiration time (currently: {}s) must be at least twice bigger than refresh time (currently: {}s)",
expiration_time.as_secs(),
refresh_time.as_secs()));
}
Ok(Self {
private_ip,
expiration_time,
refresh_time,
})
}
}

View File

@ -1,11 +1,12 @@
mod messages;
mod environment;
mod config;
mod consul;
mod consul_actor;
mod igd_actor;
mod diplonat;
mod environment;
mod fw;
mod fw_actor;
mod igd_actor;
mod messages;
use log::*;
use diplonat::Diplonat;