Skip to content

Commit

Permalink
Add hot reload support for configuration
Browse files Browse the repository at this point in the history
Reloads the configuration when it has been changed, and recreates all
collectors and observers. If an error occurs during reload, Duck will
continue to use the old configuration, but write an error to the
console output.

Closes #53
  • Loading branch information
patriksvensson committed Mar 20, 2020
1 parent 3643b05 commit 5d38a73
Show file tree
Hide file tree
Showing 29 changed files with 1,106 additions and 364 deletions.
2 changes: 1 addition & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,5 @@
**/*.rs.bk

# Configuration
config.json
/config.json
data/
6 changes: 3 additions & 3 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
url = "2.1.0"
reqwest = { version = "0.9.22", default-features = false, features = [ "rustls-tls" ] }
waithandle = "0.1.0"
waithandle = "0.3.0"
actix-web = "2"
actix-files = "0.2.1"
actix-cors = "0.2.0"
Expand Down
6 changes: 4 additions & 2 deletions src/api.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ use actix_files as fs;
use actix_web::web;
use actix_web::{App, HttpServer};
use actix_web_static_files;
use log::info;
use log::{debug, info};

mod endpoints;
mod models;
Expand Down Expand Up @@ -54,7 +54,7 @@ pub async fn start_and_block(

// Are we running embedded web?
if cfg!(feature = "embedded-web") {
info!("Serving embedded UI.");
debug!("Serving embedded UI.");
}

info!("Duck server address: {}", bind);
Expand Down Expand Up @@ -85,5 +85,7 @@ pub async fn start_and_block(
.run()
.await?;

info!("Web server stopped.");

Ok(())
}
2 changes: 1 addition & 1 deletion src/api/endpoints.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ use super::models::{BuildViewModel, ServerInfoModel, ViewInfoModel};

pub async fn server_info(state: web::Data<Arc<EngineState>>) -> HttpResponse {
let info = ServerInfoModel {
title: &state.title[..],
title: &state.ui.title()[..],
started: state
.started
.duration_since(std::time::SystemTime::UNIX_EPOCH)
Expand Down
4 changes: 4 additions & 0 deletions src/commands.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
pub mod schema;
pub mod start;
pub mod validate;

pub const DEFAULT_CONFIG: &str = "config.json";
pub const ENV_CONFIG: &str = "DUCK_CONFIG";
pub const ENV_BINDING: &str = "DUCK_BIND";
22 changes: 16 additions & 6 deletions src/commands/start.rs
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
use std::path::PathBuf;
use structopt::StructOpt;

use crate::DuckResult;
use duck::DuckResult;
use structopt::StructOpt;

pub const DEFAULT_CONFIG: &str = "config.json";
use crate::commands::{DEFAULT_CONFIG, ENV_BINDING, ENV_CONFIG};

///////////////////////////////////////////////////////////
// Arguments
Expand All @@ -16,11 +16,11 @@ pub struct Arguments {
long,
parse(from_os_str),
default_value = DEFAULT_CONFIG,
env = "DUCK_CONFIG"
env = ENV_CONFIG
)]
pub config: PathBuf,
/// The server address to bind to
#[structopt(name = "bind", short, long, env = "DUCK_BIND")]
#[structopt(name = "bind", short, long, env = ENV_BINDING)]
server_address: Option<String>,
}

Expand Down Expand Up @@ -54,6 +54,16 @@ mod tests {
// When
let config = args.config.to_str().unwrap();
// Then
assert_eq!("config.json", config);
assert_eq!(DEFAULT_CONFIG, config);
}

#[test]
pub fn default_arguments_should_have_correct_server_address() {
// Given, When
let args = Arguments::default();
// When
let server_address = args.server_address;
// Then
assert!(server_address.is_none());
}
}
32 changes: 29 additions & 3 deletions src/commands/validate.rs
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
use std::path::PathBuf;

use duck::DuckResult;
use structopt::StructOpt;

use super::start::DEFAULT_CONFIG;
use duck::DuckResult;
use crate::commands::{DEFAULT_CONFIG, ENV_CONFIG};

///////////////////////////////////////////////////////////
// Arguments
Expand All @@ -16,15 +16,41 @@ pub struct Arguments {
long,
parse(from_os_str),
default_value = DEFAULT_CONFIG,
env = "DUCK_CONFIG"
env = ENV_CONFIG
)]
pub config: PathBuf,
}

impl Default for Arguments {
fn default() -> Self {
Arguments {
config: PathBuf::from(DEFAULT_CONFIG),
}
}
}

///////////////////////////////////////////////////////////
// Command

pub async fn execute(args: Arguments) -> DuckResult<()> {
duck::validate_config(args.config).await?;
Ok(())
}

///////////////////////////////////////////////////////////
// Tests

#[cfg(test)]
mod tests {
use super::*;

#[test]
pub fn default_arguments_should_have_correct_configuration_file() {
// Given, When
let args = Arguments::default();
// When
let config = args.config.to_str().unwrap();
// Then
assert_eq!(DEFAULT_CONFIG, config);
}
}
91 changes: 28 additions & 63 deletions src/config.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
use std::path::PathBuf;

use schemars::JsonSchema;
use serde::Deserialize;

Expand All @@ -10,22 +8,31 @@ use crate::DuckResult;
mod expansions;
mod validation;

pub mod loader;

pub trait Validate {
fn validate(&self) -> DuckResult<()>;
}

#[derive(Serialize, Deserialize, JsonSchema, Clone)]
/// Represents a way of loading a configuration
pub trait ConfigurationLoader: Sync + Send + Clone {
fn exist(&self) -> bool;
fn has_changed(&self) -> DuckResult<bool>;
fn load(&self, variables: &dyn VariableProvider) -> DuckResult<Configuration>;
}

#[derive(Serialize, Deserialize, JsonSchema, Clone, Default)]
pub struct Configuration {
/// # Update interval
/// The update interval in seconds
#[serde(default)]
pub interval: Option<Interval>,
#[serde(default = "default_interval")]
pub interval: u16,
/// # Views
pub views: Option<Vec<ViewConfiguration>>,
/// # Duck frontend title
/// The title that is displayed in the UI
#[serde(default)]
pub title: Option<String>,
#[serde(default = "default_title")]
pub title: String,
/// # Collectors
pub collectors: Vec<CollectorConfiguration>,
/// # Observers
Expand All @@ -34,25 +41,8 @@ pub struct Configuration {
}

impl Configuration {
pub fn from_file(variables: &impl VariableProvider, path: PathBuf) -> DuckResult<Self> {
if !path.exists() {
if cfg!(feature = "docker") {
return Err(format_err!(
"The configuration '{}' does not exist. Have you added a Docker volume mapping?",
path.to_str().unwrap()
));
}
return Err(format_err!(
"The configuration '{}' does not exist",
path.to_str().unwrap()
));
}
let json = std::fs::read_to_string(path)?;
Configuration::from_json(variables, json)
}

pub fn from_json<T: Into<String>>(
variables: &impl VariableProvider,
variables: &dyn VariableProvider,
json: T,
) -> DuckResult<Self> {
let expander = &Expander::new(variables);
Expand All @@ -62,55 +52,19 @@ impl Configuration {
Ok(config)
}

#[cfg(test)]
pub fn empty(variables: &impl VariableProvider) -> DuckResult<Self> {
return Configuration::from_json(
variables,
r#"{
"collectors": []
}"#,
);
}

pub fn get_title(&self) -> &str {
match &self.title {
Some(title) => title,
None => "Duck",
}
}

pub fn get_interval(&self) -> u64 {
if let Some(i) = &self.interval {
if i.0 >= 15 {
return u64::from(i.0);
}
}
return 15;
}

pub fn get_all_ids(&self) -> Vec<String> {
// Get all collector id:s
let mut result: Vec<String> = self
.collectors
.iter()
.map(|i| match i {
CollectorConfiguration::TeamCity(c) => c.id.clone(),
CollectorConfiguration::Azure(c) => c.id.clone(),
CollectorConfiguration::OctopusDeploy(c) => c.id.clone(),
CollectorConfiguration::GitHub(c) => c.id.clone(),
CollectorConfiguration::AppVeyor(c) => c.id.clone(),
})
.map(|i| i.get_id().to_owned())
.collect();
// Get all observer id:s
match self.observers {
Option::None => (),
Option::Some(ref observers) => {
for observer in observers.iter() {
match observer {
ObserverConfiguration::Hue(c) => result.push(c.id.clone()),
ObserverConfiguration::Slack(c) => result.push(c.id.clone()),
ObserverConfiguration::Mattermost(c) => result.push(c.id.clone()),
};
result.push(observer.get_id().to_owned());
}
}
}
Expand Down Expand Up @@ -506,3 +460,14 @@ pub enum MattermostCredentials {
#[serde(rename = "webhook")]
Webhook { url: String },
}

///////////////////////////////////////////////////////////
// Default values

fn default_title() -> String {
"Duck".to_owned()
}

fn default_interval() -> u16 {
15
}
Loading

0 comments on commit 5d38a73

Please sign in to comment.