From ae9550ce23bbc85b05669fe5ec4406c8a67417ec Mon Sep 17 00:00:00 2001 From: LUXEY Adrien Date: Sat, 14 Aug 2021 19:12:18 +0200 Subject: 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 --- src/config/mod.rs | 11 +++++ src/config/options.rs | 75 ++++++++++++++++++++++++++++ src/config/options_test.rs | 119 +++++++++++++++++++++++++++++++++++++++++++++ src/config/runtime.rs | 112 ++++++++++++++++++++++++++++++++++++++++++ src/main.rs | 7 +-- 5 files changed, 321 insertions(+), 3 deletions(-) create mode 100644 src/config/mod.rs create mode 100644 src/config/options.rs create mode 100644 src/config/options_test.rs create mode 100644 src/config/runtime.rs (limited to 'src') diff --git a/src/config/mod.rs b/src/config/mod.rs new file mode 100644 index 0000000..14926bd --- /dev/null +++ b/src/config/mod.rs @@ -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"; \ No newline at end of file diff --git a/src/config/options.rs b/src/config/options.rs new file mode 100644 index 0000000..a76cf57 --- /dev/null +++ b/src/config/options.rs @@ -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, + /// Expiration time for IGD rules [default: 60] + pub expiration_time: Option, + /// Refresh time for IGD and Firewall rules [default: 300] + pub refresh_time: Option, +} + +/// 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, +} + +/// Consul configuration options +#[derive(Clone, Debug, Default, Deserialize)] +pub struct ConfigOptsConsul { + /// Consul's node name [default: None] + pub node_name: Option, + /// Consul's REST URL [default: "http://127.0.0.1:8500"] + pub url: Option, +} + +/// 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 { + 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: Iter) -> Result + where Iter: IntoIterator { + 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, + }) + } +} \ No newline at end of file diff --git a/src/config/options_test.rs b/src/config/options_test.rs new file mode 100644 index 0000000..7c41fce --- /dev/null +++ b/src/config/options_test.rs @@ -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 { + 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 { + 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::().unwrap() + .into()); + let refresh_time = Duration::from_secs( + opts.get(&"DIPLONAT_REFRESH_TIME".to_string()).unwrap() + .parse::().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 + ); +} \ No newline at end of file diff --git a/src/config/runtime.rs b/src/config/runtime.rs new file mode 100644 index 0000000..6649d39 --- /dev/null +++ b/src/config/runtime.rs @@ -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, + pub consul: RuntimeConfigConsul, + pub firewall: RuntimeConfigFirewall, + pub igd: RuntimeConfigIgd, +} + +impl RuntimeConfig { + pub fn new(opts: ConfigOpts) -> Result { + 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> { + if !opts.enable { + return Ok(None); + } + + let email = opts.email.unwrap(); + + Ok(Some(Self { + email, + })) + } +} + +impl RuntimeConfigConsul { + pub(super) fn new(opts: ConfigOptsConsul) -> Result { + 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 { + 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 { + 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, + }) + } +} \ No newline at end of file diff --git a/src/main.rs b/src/main.rs index ca36c26..4c4b469 100644 --- a/src/main.rs +++ b/src/main.rs @@ -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; -- cgit v1.2.3 From 644e7079562b3218243c98c89b5bcb47c1d1ab48 Mon Sep 17 00:00:00 2001 From: LUXEY Adrien Date: Mon, 16 Aug 2021 11:19:16 +0200 Subject: environment.rs successfully replaced with new config/ configuration loader. No API changes, more tests, cleaner code: life is swell. --- src/config/options.rs | 7 +++---- src/config/runtime.rs | 15 +++++++++++--- src/diplonat.rs | 28 +++++++++++++++----------- src/environment.rs | 56 --------------------------------------------------- src/igd_actor.rs | 4 ++-- src/main.rs | 1 - 6 files changed, 33 insertions(+), 78 deletions(-) delete mode 100644 src/environment.rs (limited to 'src') diff --git a/src/config/options.rs b/src/config/options.rs index a76cf57..27b1af1 100644 --- a/src/config/options.rs +++ b/src/config/options.rs @@ -9,7 +9,7 @@ use crate::config::RuntimeConfig; // runtime.rs applies business logic and builds RuntimeConfig structs. /// Base configuration options -#[derive(Clone, Debug, Default, Deserialize)] +#[derive(Clone, Default, Deserialize)] pub struct ConfigOptsBase { /// This node's private IP address [default: None] pub private_ip: Option, @@ -20,7 +20,7 @@ pub struct ConfigOptsBase { } /// ACME configuration options -#[derive(Clone, Debug, Default, Deserialize)] +#[derive(Clone, Default, Deserialize)] pub struct ConfigOptsAcme { /// Whether ACME is enabled [default: false] #[serde(default)] @@ -31,7 +31,7 @@ pub struct ConfigOptsAcme { } /// Consul configuration options -#[derive(Clone, Debug, Default, Deserialize)] +#[derive(Clone, Default, Deserialize)] pub struct ConfigOptsConsul { /// Consul's node name [default: None] pub node_name: Option, @@ -40,7 +40,6 @@ pub struct ConfigOptsConsul { } /// Model of all potential configuration options -#[derive(Debug)] pub struct ConfigOpts { pub base: ConfigOptsBase, pub acme: ConfigOptsAcme, diff --git a/src/config/runtime.rs b/src/config/runtime.rs index 6649d39..58c86b9 100644 --- a/src/config/runtime.rs +++ b/src/config/runtime.rs @@ -9,25 +9,30 @@ use crate::config::{ConfigOpts, ConfigOptsAcme, ConfigOptsBase, ConfigOptsConsul // In this file, we take ConfigOpts and transform them into ready-to-use RuntimeConfig. // We apply default values and business logic. +#[derive(Debug)] pub struct RuntimeConfigAcme { pub email: String, } +#[derive(Debug)] pub struct RuntimeConfigConsul { pub node_name: String, pub url: String, } +#[derive(Debug)] pub struct RuntimeConfigFirewall { pub refresh_time: Duration, } +#[derive(Debug)] pub struct RuntimeConfigIgd { pub private_ip: String, pub expiration_time: Duration, pub refresh_time: Duration, } +#[derive(Debug)] pub struct RuntimeConfig { pub acme: Option, pub consul: RuntimeConfigConsul, @@ -57,7 +62,9 @@ impl RuntimeConfigAcme { return Ok(None); } - let email = opts.email.unwrap(); + let email = opts.email.expect( + "'DIPLONAT_ACME_EMAIL' environment variable is required \ + if 'DIPLONAT_ACME_ENABLE' == 'true'"); Ok(Some(Self { email, @@ -67,7 +74,8 @@ impl RuntimeConfigAcme { impl RuntimeConfigConsul { pub(super) fn new(opts: ConfigOptsConsul) -> Result { - let node_name = opts.node_name.unwrap(); + let node_name = opts.node_name.expect( + "'DIPLONAT_CONSUL_NODE_NAME' environment variable is required"); let url = opts.url.unwrap_or(super::CONSUL_URL.to_string()); Ok(Self { @@ -90,7 +98,8 @@ impl RuntimeConfigFirewall { impl RuntimeConfigIgd { pub(super) fn new(opts: ConfigOptsBase) -> Result { - let private_ip = opts.private_ip.unwrap(); + let private_ip = opts.private_ip.expect( + "'DIPLONAT_PRIVATE_IP' environment variable is required"); let expiration_time = Duration::from_secs( opts.expiration_time.unwrap_or(super::EXPIRATION_TIME).into()); let refresh_time = Duration::from_secs( diff --git a/src/diplonat.rs b/src/diplonat.rs index 798b779..7049530 100644 --- a/src/diplonat.rs +++ b/src/diplonat.rs @@ -1,31 +1,35 @@ use anyhow::Result; use tokio::try_join; + +use crate::config::ConfigOpts; use crate::consul_actor::ConsulActor; -use crate::igd_actor::IgdActor; -use crate::environment::Environment; use crate::fw_actor::FirewallActor; +use crate::igd_actor::IgdActor; pub struct Diplonat { consul: ConsulActor, + firewall: FirewallActor, igd: IgdActor, - firewall: FirewallActor } impl Diplonat { pub async fn new() -> Result { - let env = Environment::new()?; - let ca = ConsulActor::new(&env.consul_url, &env.consul_node_name); - let ia = IgdActor::new( - &env.private_ip, - env.refresh_time, - env.expiration_time, - &ca.rx_open_ports - ).await?; + let rt_cfg = ConfigOpts::from_env()?; + println!("{:#?}", rt_cfg); + + let ca = ConsulActor::new(&rt_cfg.consul.url, &rt_cfg.consul.node_name); let fw = FirewallActor::new( - env.refresh_time, + rt_cfg.firewall.refresh_time, &ca.rx_open_ports ).await?; + + let ia = IgdActor::new( + &rt_cfg.igd.private_ip, + rt_cfg.igd.refresh_time, + rt_cfg.igd.expiration_time, + &ca.rx_open_ports + ).await?; let ctx = Self { consul: ca, diff --git a/src/environment.rs b/src/environment.rs deleted file mode 100644 index 335fa37..0000000 --- a/src/environment.rs +++ /dev/null @@ -1,56 +0,0 @@ -use std::env; -use anyhow::{Result, Context, anyhow}; -use std::time::Duration; -use log::*; - -const epi: &'static str = "DIPLONAT_PRIVATE_IP"; -const ert: &'static str = "DIPLONAT_REFRESH_TIME"; -const eet: &'static str = "DIPLONAT_EXPIRATION_TIME"; -const ecnd: &'static str = "DIPLONAT_CONSUL_NODE_NAME"; -const ecu: &'static str = "DIPLONAT_CONSUL_URL"; - -pub struct Environment { - pub consul_node_name: String, - pub consul_url: String, - - pub refresh_time: Duration, - pub expiration_time: Duration, - - pub private_ip: String, -} - -/* @FIXME: Rewrite with Serde Envi */ -impl Environment { - pub fn new() -> Result { - let ctx = Self { - consul_url: match env::var(ecu) { Ok(e) => e, Err(_) => "http://127.0.0.1:8500".to_string() }, - consul_node_name: env::var(ecnd).with_context(|| format!("{} env var must be defined", ecnd))?, - private_ip: env::var(epi).with_context(|| format!("{} env var must be defined, eg: 192.168.0.18", epi))?, - refresh_time: Duration::from_secs(env::var(ert) - .with_context(|| format!("{} env var must be defined, eg: 60", ert))? - .parse() - .with_context(|| format!("{} env var must be an integer, eg: 60", ert))?), - expiration_time: Duration::from_secs(env::var(eet) - .with_context(|| format!("{} env var must be defined, eg: 300", eet))? - .parse() - .with_context(|| format!("{} env var must be an integer, eg: 300", eet))?), - }; - - if ctx.refresh_time.as_secs() * 2 > ctx.expiration_time.as_secs() { - return Err(anyhow!( - "Expiration time (currently: {}s) must be twice bigger than refresh time (currently: {}s)", - ctx.refresh_time.as_secs(), - ctx.expiration_time.as_secs())); - } - - info!("Consul URL: {:#?}", ctx.consul_url); - info!("Consul node name: {:#?}", ctx.consul_node_name); - info!("Private IP address: {:#?}", ctx.private_ip); - info!("Refresh time: {:#?} seconds", ctx.refresh_time.as_secs()); - info!("Expiration time: {:#?} seconds", ctx.expiration_time.as_secs()); - - return Ok(ctx); - } -} - - diff --git a/src/igd_actor.rs b/src/igd_actor.rs index 68d20df..55d9c5f 100644 --- a/src/igd_actor.rs +++ b/src/igd_actor.rs @@ -25,8 +25,8 @@ impl IgdActor { pub async fn new(priv_ip: &str, refresh: Duration, expire: Duration, rxp: &watch::Receiver) -> Result { let gw = search_gateway(Default::default()) .await - .context("Failed to find gateway")?; - info!("Gateway: {}", gw); + .context("Failed to find IGD gateway")?; + info!("IGD gateway: {}", gw); let ctx = Self { gateway: gw, diff --git a/src/main.rs b/src/main.rs index 4c4b469..720edf8 100644 --- a/src/main.rs +++ b/src/main.rs @@ -2,7 +2,6 @@ mod config; mod consul; mod consul_actor; mod diplonat; -mod environment; mod fw; mod fw_actor; mod igd_actor; -- cgit v1.2.3 From 0f114f21344ddb59f50fed68540c54c41300cf51 Mon Sep 17 00:00:00 2001 From: LUXEY Adrien Date: Mon, 16 Aug 2021 11:34:42 +0200 Subject: a last pass to remove warnings from my code --- src/config/options.rs | 2 ++ src/config/options_test.rs | 7 ++++--- 2 files changed, 6 insertions(+), 3 deletions(-) (limited to 'src') diff --git a/src/config/options.rs b/src/config/options.rs index 27b1af1..36da475 100644 --- a/src/config/options.rs +++ b/src/config/options.rs @@ -59,6 +59,8 @@ impl ConfigOpts { }) } + // Currently only used in tests + #[allow(dead_code)] pub fn from_iter(iter: Iter) -> Result where Iter: IntoIterator { let base: ConfigOptsBase = envy::prefixed("DIPLONAT_").from_iter(iter.clone())?; diff --git a/src/config/options_test.rs b/src/config/options_test.rs index 7c41fce..a6063fd 100644 --- a/src/config/options_test.rs +++ b/src/config/options_test.rs @@ -1,5 +1,4 @@ use std::collections::HashMap; -use std::env; use std::time::Duration; use crate::config::*; @@ -30,7 +29,9 @@ fn all_valid_options() -> HashMap { #[test] #[should_panic] fn err_empty_env() { - ConfigOpts::from_env(); + std::env::remove_var("DIPLONAT_PRIVATE_IP"); + std::env::remove_var("DIPLONAT_CONSUL_NODE_NAME"); + ConfigOpts::from_env().unwrap(); } #[test] @@ -71,7 +72,7 @@ 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(); + ConfigOpts::from_iter(opts).unwrap(); } #[test] -- cgit v1.2.3