reorganize + some documentation

This commit is contained in:
Michael Zhang 2018-09-01 14:56:58 -05:00
parent 4c67a56b61
commit 6d9f9b178a
No known key found for this signature in database
GPG key ID: A1B65B603268116B
6 changed files with 97 additions and 75 deletions

2
Cargo.lock generated
View file

@ -184,7 +184,7 @@ dependencies = [
[[package]] [[package]]
name = "dip" name = "dip"
version = "0.1.0" version = "0.1.3"
dependencies = [ dependencies = [
"failure 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)", "failure 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)",
"futures 0.1.23 (registry+https://github.com/rust-lang/crates.io-index)", "futures 0.1.23 (registry+https://github.com/rust-lang/crates.io-index)",

View file

@ -1,11 +1,19 @@
//! Configuration.
use std::default::Default;
use std::env;
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};
use std::sync::mpsc;
use std::time::Duration;
use failure::{err_msg, Error}; use failure::{err_msg, Error};
use notify::{self, RecommendedWatcher, RecursiveMode, Watcher};
use walkdir::WalkDir; use walkdir::WalkDir;
use Hook; use Hook;
use {HOOKS, PROGRAMS}; use {HOOKS, PROGRAMS};
/// The configuration to be parsed from the command line.
#[derive(Debug, StructOpt)] #[derive(Debug, StructOpt)]
pub struct Config { pub struct Config {
/// The root configuration directory for dip. This argument is required. /// The root configuration directory for dip. This argument is required.
@ -20,27 +28,38 @@ pub struct Config {
pub hook: Option<String>, pub hook: Option<String>,
} }
impl Config { impl Default for Config {
pub fn new(root: impl AsRef<Path>) -> Self { fn default() -> Self {
let root = root.as_ref().to_path_buf(); let root = env::current_dir().unwrap();
assert!(root.exists()); assert!(root.exists());
let bind = "0.0.0.0:5000".to_owned(); let bind = "0.0.0.0:5000".to_owned();
let hook = None; let hook = None;
Config { root, bind, hook } Config { root, bind, hook }
} }
pub fn bind(mut self, value: Option<String>) -> Config { }
if let Some(value) = value {
self.bind = value; pub(crate) fn watch<P>(root: P) -> notify::Result<()>
where
P: AsRef<Path>,
{
let (tx, rx) = mpsc::channel();
let mut watcher: RecommendedWatcher = Watcher::new(tx, Duration::from_secs(1))?;
println!("Watching {:?}", root.as_ref().to_path_buf());
watcher.watch(root.as_ref(), RecursiveMode::Recursive)?;
loop {
match rx.recv() {
Ok(_) => {
// for now, naively reload entire config every time
// TODO: don't do this
load_config(root.as_ref())
} }
return self; Err(e) => eprintln!("watch error: {:?}", e),
} }
pub fn hook(mut self, value: Option<String>) -> Config {
self.hook = value;
return self;
} }
} }
/// Load config from the root directory. This is called by the watcher.
pub fn load_config<P>(root: P) pub fn load_config<P>(root: P)
where where
P: AsRef<Path>, P: AsRef<Path>,
@ -48,7 +67,13 @@ where
println!("Reloading config..."); println!("Reloading config...");
// hold on to the lock while config is being reloaded // hold on to the lock while config is being reloaded
{ {
let mut programs = PROGRAMS.lock().unwrap(); let mut programs = match PROGRAMS.lock() {
Ok(programs) => programs,
Err(err) => {
eprintln!("Could not acquire programs lock: {}", err);
return;
}
};
// TODO: some kind of smart diff // TODO: some kind of smart diff
programs.clear(); programs.clear();
@ -80,7 +105,13 @@ where
} }
} }
{ {
let mut hooks = HOOKS.lock().unwrap(); let mut hooks = match HOOKS.lock() {
Ok(hooks) => hooks,
Err(err) => {
eprintln!("Could not acquire hooks lock: {}", err);
return;
}
};
hooks.clear(); hooks.clear();
let hooks_dir = { let hooks_dir = {
let mut p = root.as_ref().to_path_buf(); let mut p = root.as_ref().to_path_buf();

View file

@ -15,16 +15,21 @@ use toml::Value as TomlValue;
use github; use github;
/// A single instance of handler as defined by the config.
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
pub struct Handler { pub struct Handler {
pub config: TomlValue, pub(crate) config: TomlValue,
pub action: Action, pub(crate) action: Action,
} }
/// Describes an action that a hook can take.
#[derive(Clone)] #[derive(Clone)]
pub enum Action { pub enum Action {
/// A builtin function (for example, the Github handler).
Builtin(fn(&TomlValue, &JsonValue) -> Result<JsonValue, Error>), Builtin(fn(&TomlValue, &JsonValue) -> Result<JsonValue, Error>),
/// A command represents a string to be executed by `bash -c`.
Command(String), Command(String),
/// A program represents one of the handlers specified in the `handlers` directory.
Program(String), Program(String),
} }
@ -38,10 +43,7 @@ impl fmt::Debug for Action {
} }
impl Handler { impl Handler {
pub fn config(&self) -> &TomlValue { pub(crate) fn from(config: &TomlValue) -> Result<Self, Error> {
&self.config
}
pub fn from(config: &TomlValue) -> Result<Self, Error> {
let handler = config let handler = config
.get("type") .get("type")
.ok_or(err_msg("No 'type' found."))? .ok_or(err_msg("No 'type' found."))?
@ -57,23 +59,13 @@ impl Handler {
Action::Command(command.to_owned()) Action::Command(command.to_owned())
} }
"github" => Action::Builtin(github::main), "github" => Action::Builtin(github::main),
handler => { handler => Action::Program(handler.to_owned()),
// let programs = HANDLERS.lock().unwrap();
// let program = programs
// .get(handler)
// .ok_or(err_msg(format!("'{}' is not a valid executable", handler)))
// .and_then(|value| {
// value
// .canonicalize()
// .map_err(|_| err_msg("failed to canonicalize the path"))
// }).map(|value| value.clone())?;
Action::Program(handler.to_owned())
}
}; };
let config = config.clone(); let config = config.clone();
Ok(Handler { config, action }) Ok(Handler { config, action })
} }
/// Runs the given [action](Action) and produces a [Future](Future).
pub fn run( pub fn run(
config: TomlValue, config: TomlValue,
action: Action, action: Action,
@ -93,7 +85,11 @@ impl Handler {
let command_helper = move |command: &mut Command| { let command_helper = move |command: &mut Command| {
command command
.current_dir(&temp_path) .current_dir(&temp_path)
.env("DIP_WORKDIR", &temp_path); .env("DIP_ROOT", "lol")
.env("DIP_WORKDIR", &temp_path)
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.stderr(Stdio::piped());
}; };
let output: Box<Future<Item = JsonValue, Error = Error> + Send> = match action { let output: Box<Future<Item = JsonValue, Error = Error> + Send> = match action {
@ -105,13 +101,7 @@ impl Handler {
// TODO: allow some kind of simple variable replacement // TODO: allow some kind of simple variable replacement
let mut command = Command::new("/bin/bash"); let mut command = Command::new("/bin/bash");
command_helper(&mut command); command_helper(&mut command);
let child = command let child = command.arg("-c").arg(cmd);
.env("DIP_ROOT", "lol")
.arg("-c")
.arg(cmd)
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.stderr(Stdio::piped());
let result = child let result = child
.output_async() .output_async()
.map_err(|err| err_msg(format!("failed to spawn child: {}", err))) .map_err(|err| err_msg(format!("failed to spawn child: {}", err)))
@ -131,12 +121,8 @@ impl Handler {
let mut command = Command::new(&path); let mut command = Command::new(&path);
command_helper(&mut command); command_helper(&mut command);
let mut child = command let mut child = command
.env("DIP_ROOT", "")
.arg("--config") .arg("--config")
.arg(config_str) .arg(config_str)
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.spawn_async() .spawn_async()
.expect("could not spawn child"); .expect("could not spawn child");

View file

@ -1,3 +1,5 @@
//! The webhook.
use std::fs::File; use std::fs::File;
use std::io::Read; use std::io::Read;
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};
@ -10,12 +12,14 @@ use toml::Value;
use Handler; use Handler;
/// A webhook.
pub struct Hook { pub struct Hook {
name: String, name: String,
handlers: Vec<Handler>, handlers: Vec<Handler>,
} }
impl Hook { impl Hook {
/// Creates a hook from a (name, config) pair.
pub fn from(name: impl Into<String>, config: &Value) -> Result<Self, Error> { pub fn from(name: impl Into<String>, config: &Value) -> Result<Self, Error> {
let name = name.into(); let name = name.into();
let handlers = config let handlers = config
@ -28,6 +32,8 @@ impl Hook {
.collect::<Result<Vec<_>, _>>()?; .collect::<Result<Vec<_>, _>>()?;
Ok(Hook { name, handlers }) Ok(Hook { name, handlers })
} }
/// Creates a hook from a configuration file.
pub fn from_file<P>(path: P) -> Result<Hook, Error> pub fn from_file<P>(path: P) -> Result<Hook, Error>
where where
P: AsRef<Path>, P: AsRef<Path>,
@ -47,10 +53,13 @@ impl Hook {
let hook = Hook::from(filename, &config)?; let hook = Hook::from(filename, &config)?;
Ok(hook) Ok(hook)
} }
/// Gets the name of this hook.
pub fn get_name(&self) -> String { pub fn get_name(&self) -> String {
self.name.clone() self.name.clone()
} }
pub fn handle(&self, req: JsonValue, temp_path: PathBuf) -> Result<String, String> {
pub(crate) fn handle(&self, req: JsonValue, temp_path: PathBuf) -> Result<String, String> {
let handlers = self let handlers = self
.handlers .handlers
.iter() .iter()

View file

@ -1,4 +1,19 @@
//! # Dip //! # Dip
//!
//! The configurable webhook server. Latest stable binary releases for Linux are available on the [releases][1] page.
//!
//! ## Getting Started
//!
//! Setup is incredibly simple: first, obtain a copy of `dip` either through the binary releases page or by compiling from source.
//! Then, create a directory that you'll use as your `DIP_ROOT` directory. It should look like this:
//!
//! ```text
//!
//! ```
//!
//! [1]: https://github.com/acmumn/dip/releases
#![deny(missing_docs)]
extern crate hmac; extern crate hmac;
extern crate secstr; extern crate secstr;
@ -26,24 +41,22 @@ extern crate toml;
extern crate walkdir; extern crate walkdir;
pub mod config; pub mod config;
pub mod github; mod github;
pub mod handler; mod handler;
pub mod hook; pub mod hook;
pub mod service; mod service;
use std::collections::HashMap; use std::collections::HashMap;
use std::net::SocketAddrV4; use std::net::SocketAddrV4;
use std::path::{Path, PathBuf}; use std::path::PathBuf;
use std::str::FromStr; use std::str::FromStr;
use std::sync::{mpsc, Arc, Mutex}; use std::sync::{Arc, Mutex};
use std::thread; use std::thread;
use std::time::Duration;
use failure::Error; use failure::Error;
use hyper::rt::Future; use hyper::rt::Future;
use hyper::service::service_fn; use hyper::service::service_fn;
use hyper::Server; use hyper::Server;
use notify::{RecommendedWatcher, RecursiveMode, Watcher};
use regex::Regex; use regex::Regex;
pub use config::Config; pub use config::Config;
@ -54,38 +67,19 @@ use service::*;
const URIPATTERN_STR: &str = r"/webhook/(?P<name>[A-Za-z._][A-Za-z0-9._]*)"; const URIPATTERN_STR: &str = r"/webhook/(?P<name>[A-Za-z._][A-Za-z0-9._]*)";
lazy_static! { lazy_static! {
static ref URIPATTERN: Regex = Regex::new(URIPATTERN_STR).unwrap(); static ref URIPATTERN: Regex =
Regex::new(URIPATTERN_STR).expect("Could not compile regular expression.");
static ref PROGRAMS: Arc<Mutex<HashMap<String, PathBuf>>> = static ref PROGRAMS: Arc<Mutex<HashMap<String, PathBuf>>> =
Arc::new(Mutex::new(HashMap::new())); Arc::new(Mutex::new(HashMap::new()));
static ref HOOKS: Arc<Mutex<HashMap<String, Hook>>> = Arc::new(Mutex::new(HashMap::new())); static ref HOOKS: Arc<Mutex<HashMap<String, Hook>>> = Arc::new(Mutex::new(HashMap::new()));
} }
fn watch<P>(root: P) -> notify::Result<()>
where
P: AsRef<Path>,
{
let (tx, rx) = mpsc::channel();
let mut watcher: RecommendedWatcher = Watcher::new(tx, Duration::from_secs(1))?;
println!("Watching {:?}", root.as_ref().to_path_buf());
watcher.watch(root.as_ref(), RecursiveMode::Recursive)?;
loop {
match rx.recv() {
Ok(_) => {
// for now, naively reload entire config every time
// TODO: don't do this
config::load_config(root.as_ref())
}
Err(e) => eprintln!("watch error: {:?}", e),
}
}
}
/// Main entry point of the entire application. /// Main entry point of the entire application.
pub fn run(config: &Config) -> Result<(), Error> { pub fn run(config: &Config) -> Result<(), Error> {
config::load_config(&config.root); config::load_config(&config.root);
let v = config.root.clone(); let v = config.root.clone();
thread::spawn(|| watch(v)); thread::spawn(|| config::watch(v));
let addr: SocketAddrV4 = SocketAddrV4::from_str(config.bind.as_ref())?; let addr: SocketAddrV4 = SocketAddrV4::from_str(config.bind.as_ref())?;
let server = Server::bind(&addr.into()) let server = Server::bind(&addr.into())

View file

@ -6,7 +6,9 @@ use mktemp::Temp;
use {HOOKS, URIPATTERN}; use {HOOKS, URIPATTERN};
pub fn dip_service(req: Request<Body>) -> Box<Future<Item = Response<Body>, Error = Error> + Send> { pub(crate) fn dip_service(
req: Request<Body>,
) -> Box<Future<Item = Response<Body>, Error = Error> + Send> {
let path = req.uri().path().to_owned(); let path = req.uri().path().to_owned();
let captures = match URIPATTERN.captures(path.as_ref()) { let captures = match URIPATTERN.captures(path.as_ref()) {
Some(value) => value, Some(value) => value,