From 321dcb6877297feb3780e41899903ef159f5f0fa Mon Sep 17 00:00:00 2001 From: Zoran Zaric Date: Sat, 7 Sep 2019 17:12:17 +0200 Subject: [PATCH] init --- .gitignore | 3 + Cargo.toml | 16 +++ src/lib.rs | 298 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 317 insertions(+) create mode 100644 .gitignore create mode 100644 Cargo.toml create mode 100644 src/lib.rs diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..6936990 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +/target +**/*.rs.bk +Cargo.lock diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..430c27c --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,16 @@ +[package] +name = "deploy-config" +version = "0.1.0" +authors = ["Zoran Zaric "] +edition = "2018" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +serde = { version = "1.0.99", features = [ "derive" ] } +ssh2 = "0.3.3" +url = "2.1.0" +serde_yaml = "0.8.9" +serde_json = "1.0.40" +serde_with = "1.3.1" +reqwest = "0.9.20" diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..42382bb --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,298 @@ +use serde::{Deserialize, Serialize}; +use std::collections::HashMap; +use std::fmt::{self, Display}; +use std::path::PathBuf; +use std::process::Command; +use std::str::FromStr; +use url::Url; + +#[derive(Debug, PartialEq, Deserialize, Serialize)] +pub struct DeployConfig { + tasks: Vec, +} + +pub trait Deploy { + fn deploy(&self) -> Result<(), ()>; +} + +#[derive(Debug, PartialEq, Deserialize, Serialize)] +pub enum Task { + #[serde(rename = "copy")] + Copy { + source: String, + + #[serde(with = "serde_with::rust::display_fromstr")] + target: Target, + }, + + #[serde(rename = "trigger")] + Trigger { + #[serde(with = "serde_with::rust::display_fromstr")] + method: TriggerMethod, + url: String, + parameters: Vec, + }, +} + +impl Deploy for Task { + fn deploy(&self) -> Result<(), ()> { + match self { + Self::Copy { source, target } => match target.deploy(PathBuf::from(source)) { + Ok(_) => Ok(()), + Err(_) => Err(()), + }, + Self::Trigger { + method, + url, + parameters, + } => match method { + &TriggerMethod::HttpGet => { + let params: Vec<_> = parameters + .iter() + .map(|ref p| (p.name.clone(), p.value.clone())) + .collect(); + + let resp = reqwest::Client::new().get(url).query(¶ms).send(); + + match resp { + Ok(resp) => { + if resp.status().is_success() { + Ok(()) + } else { + Err(()) + } + } + Err(_) => Err(()), + } + } + &TriggerMethod::HttpPost => { + let params: HashMap<_, _> = parameters + .iter() + .map(|ref p| (p.name.clone(), p.value.clone())) + .collect(); + + let resp = reqwest::Client::new().post(url).form(¶ms).send(); + + match resp { + Ok(resp) => { + if resp.status().is_success() { + Ok(()) + } else { + Err(()) + } + } + Err(_) => Err(()), + } + } + }, + } + } +} + +#[derive(Debug, PartialEq)] +pub enum Target { + Scp { + username: Option, + host: String, + port: Option, + path: Option, + }, +} + +impl FromStr for Target { + type Err = String; + fn from_str(s: &str) -> Result { + match Url::parse(s) { + Ok(url) => match url.scheme() { + "scp" => { + let username = if url.username().is_empty() { + None + } else { + Some(url.username().into()) + }; + let host = match url.host_str() { + Some(host_str) => host_str.into(), + None => return Err("Host is missing".into()), + }; + let port = url.port(); + let path = if url.path().is_empty() { + None + } else { + Some(url.path().into()) + }; + + Ok(Self::Scp { + username, + host, + port, + path, + }) + } + scheme => Err(format!("Unsupported scheme \"{}\"", scheme)), + }, + Err(e) => Err(format!("{}", e)), + } + } +} + +impl Display for Target { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + // FIXME + write!(f, "foo") + } +} + +impl Target { + pub fn deploy(&self, source: PathBuf) -> Result<(), std::io::Error> { + match self { + Self::Scp { + username, + host, + port, + path, + } => { + let mut cmd = Command::new("scp"); + + let source: String = source.to_string_lossy().into(); + let cmd = cmd.arg(source); + + let cmd = if let Some(port) = port { + cmd.arg("-P").arg(format!("{}", port)) + } else { + cmd + }; + + let mut s = String::new(); + + if let Some(username) = username { + s.push_str(&format!("{}@", username)); + } + + s.push_str(&format!("{}:", host)); + + if let Some(path) = path { + s.push_str(path); + } + + match cmd.output() { + Ok(output) => { + if output.status.success() { + Ok(()) + } else { + Err(std::io::Error::new( + std::io::ErrorKind::Other, + format!( + "scp command failed with exit code {:?}", + output.status.code() + ), + )) + } + } + Err(e) => Err(e), + } + } + } + } +} + +#[derive(Debug, PartialEq)] +pub enum TriggerMethod { + HttpGet, + HttpPost, +} + +impl FromStr for TriggerMethod { + type Err = String; + fn from_str(s: &str) -> Result { + match s { + "http-get" => Ok(Self::HttpGet), + "http-post" => Ok(Self::HttpPost), + trigger_method => Err(format!("Unsupported trigger-method \"{}\"", trigger_method)), + } + } +} + +impl Display for TriggerMethod { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::HttpGet => write!(f, "http-get"), + Self::HttpPost => write!(f, "http-post"), + } + } +} + +#[derive(Debug, PartialEq, Deserialize, Serialize)] +pub struct TriggerParameter { + name: String, + value: String, +} + +#[cfg(test)] +mod test { + use super::*; + + #[test] + pub fn test_parse_target() { + let target = dbg!(Target::from_str("scp://foo@bar:/vol1/asdf")).unwrap(); + assert_eq!( + Target::Scp { + username: Some("foo".into()), + host: "bar".into(), + port: None, + path: Some("/vol1/asdf".into()), + }, + target + ); + } + + #[test] + pub fn test_parse() { + let s = r#"--- +tasks: +- copy: + source: hermes.war + target: scp://compaxdev@delivery.comi.run:/vol1/delivery/aax2/microservices/tuadev/hermes/hermes.war +- trigger: + method: http-get + url: http://172.29.110.20:8080/job/00_congo_neue_tuad_microservices_deployment_trigger_compax/buildWithParameters + parameters: + - name: token + value: up9ul9oph5augh2oz8oak3aiSeemaxa + - name: ms_visibility + value: cob2b + - name: warfile + value: hermes +"#; + assert_eq!( + DeployConfig { + tasks: vec![Task::Copy { + source: "hermes.war".into(), + target: Target::Scp { + username: Some("compaxdev".into()), + host: "delivery.comi.run".into(), + port: None, + path: Some("/vol1/delivery/aax2/microservices/tuadev/hermes/hermes.war".into()), + } + }, Task::Trigger { + method: TriggerMethod::HttpGet, + url: "http://172.29.110.20:8080/job/00_congo_neue_tuad_microservices_deployment_trigger_compax/buildWithParameters".into(), + parameters: vec![ + TriggerParameter { + name: "token".into(), + value: "up9ul9oph5augh2oz8oak3aiSeemaxa".into(), + }, + TriggerParameter { + name: "ms_visibility".into(), + value: "cob2b".into(), + }, + TriggerParameter { + name: "warfile".into(), + value: "hermes".into(), + }, + ], + }], + }, + serde_yaml::from_str(&s).unwrap() + ); + } +}