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]]
name = "dip"
version = "0.1.0"
version = "0.1.3"
dependencies = [
"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)",

View file

@ -1,11 +1,19 @@
//! Configuration.
use std::default::Default;
use std::env;
use std::path::{Path, PathBuf};
use std::sync::mpsc;
use std::time::Duration;
use failure::{err_msg, Error};
use notify::{self, RecommendedWatcher, RecursiveMode, Watcher};
use walkdir::WalkDir;
use Hook;
use {HOOKS, PROGRAMS};
/// The configuration to be parsed from the command line.
#[derive(Debug, StructOpt)]
pub struct Config {
/// The root configuration directory for dip. This argument is required.
@ -20,27 +28,38 @@ pub struct Config {
pub hook: Option<String>,
}
impl Config {
pub fn new(root: impl AsRef<Path>) -> Self {
let root = root.as_ref().to_path_buf();
impl Default for Config {
fn default() -> Self {
let root = env::current_dir().unwrap();
assert!(root.exists());
let bind = "0.0.0.0:5000".to_owned();
let hook = None;
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())
}
Err(e) => eprintln!("watch error: {:?}", e),
}
return self;
}
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)
where
P: AsRef<Path>,
@ -48,7 +67,13 @@ where
println!("Reloading config...");
// 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
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();
let hooks_dir = {
let mut p = root.as_ref().to_path_buf();

View file

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

View file

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

View file

@ -1,4 +1,19 @@
//! # 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 secstr;
@ -26,24 +41,22 @@ extern crate toml;
extern crate walkdir;
pub mod config;
pub mod github;
pub mod handler;
mod github;
mod handler;
pub mod hook;
pub mod service;
mod service;
use std::collections::HashMap;
use std::net::SocketAddrV4;
use std::path::{Path, PathBuf};
use std::path::PathBuf;
use std::str::FromStr;
use std::sync::{mpsc, Arc, Mutex};
use std::sync::{Arc, Mutex};
use std::thread;
use std::time::Duration;
use failure::Error;
use hyper::rt::Future;
use hyper::service::service_fn;
use hyper::Server;
use notify::{RecommendedWatcher, RecursiveMode, Watcher};
use regex::Regex;
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._]*)";
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>>> =
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.
pub fn run(config: &Config) -> Result<(), Error> {
config::load_config(&config.root);
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 server = Server::bind(&addr.into())

View file

@ -6,7 +6,9 @@ use mktemp::Temp;
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 captures = match URIPATTERN.captures(path.as_ref()) {
Some(value) => value,