From ae9550ce23bbc85b05669fe5ec4406c8a67417ec Mon Sep 17 00:00:00 2001 From: LUXEY Adrien Date: Sat, 14 Aug 2021 19:12:18 +0200 Subject: [PATCH] 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 --- Cargo.lock | 10 ++++ Cargo.toml | 17 +++--- 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 ++- 7 files changed, 340 insertions(+), 11 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 diff --git a/Cargo.lock b/Cargo.lock index 7a050b4..bd1fca3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -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" diff --git a/Cargo.toml b/Cargo.toml index a2a9667..b99d81a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -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" 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;