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]]
|
||||
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)",
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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");
|
||||
|
||||
|
|
11
src/hook.rs
11
src/hook.rs
|
@ -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()
|
||||
|
|
52
src/lib.rs
52
src/lib.rs
|
@ -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())
|
||||
|
|
|
@ -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,
|
||||
|
|
Loading…
Reference in a new issue