reorganize + some documentation
This commit is contained in:
parent
4c67a56b61
commit
6d9f9b178a
6 changed files with 97 additions and 75 deletions
2
Cargo.lock
generated
2
Cargo.lock
generated
|
@ -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)",
|
||||||
|
|
|
@ -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())
|
||||||
|
}
|
||||||
|
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)
|
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();
|
||||||
|
|
|
@ -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");
|
||||||
|
|
||||||
|
|
11
src/hook.rs
11
src/hook.rs
|
@ -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()
|
||||||
|
|
52
src/lib.rs
52
src/lib.rs
|
@ -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())
|
||||||
|
|
|
@ -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,
|
||||||
|
|
Loading…
Reference in a new issue