extern crate reqwest;
extern crate url;
use std::collections::BTreeMap;
use std::io::Read;
use std::sync::Arc;
use std::thread::sleep;
use std::time::Duration;
use reqwest::header::{Authorization, Basic, Headers, Location};
use reqwest::{Client, IntoUrl, Response};
use serde_json::{self, Value};
use patchwork::TestState;
const JENKINS_POLLING_INTERVAL: u64 = 5000;
pub trait CIBackend {
fn start_test(&self, job_name: &str, params: Vec<(&str, &str)>)
-> Result<String, &'static str>;
}
pub struct JenkinsBackend {
pub base_url: String,
pub reqwest_client: Arc<Client>,
pub username: Option<String>,
pub token: Option<String>,
}
impl CIBackend for JenkinsBackend {
fn start_test(
&self,
job_name: &str,
params: Vec<(&str, &str)>,
) -> Result<String, &'static str> {
let params = url::form_urlencoded::Serializer::new(String::new())
.extend_pairs(params)
.finish();
let resp =
self.post_url(&format!(
"{}/job/{}/buildWithParameters?{}",
self.base_url, job_name, params
)).expect("HTTP request error");
match resp.headers().get::<Location>() {
Some(loc) => Ok(loc.to_string()),
None => Err("No Location header returned"),
}
}
}
#[derive(Eq, PartialEq)]
pub enum JenkinsBuildStatus {
Running,
Done,
}
impl JenkinsBackend {
fn headers(&self) -> Headers {
let mut headers = Headers::new();
if let Some(ref username) = self.username {
headers.set(Authorization(Basic {
username: username.clone(),
password: self.token.clone(),
}));
}
headers
}
fn get_url<U: IntoUrl>(&self, url: U) -> Result<Response, reqwest::Error> {
self.reqwest_client.get(url).headers(self.headers()).send()
}
fn post_url<U: IntoUrl>(&self, url: U) -> Result<Response, reqwest::Error> {
self.reqwest_client.post(url).headers(self.headers()).send()
}
fn get_api_json_object(&self, base_url: &str) -> Value {
let url = format!("{}api/json", base_url);
let mut result_str = String::new();
loop {
let mut resp = match self.get_url(&url) {
Ok(r) => r,
Err(e) => {
warn!("Couldn't hit Jenkins API: {}", e);
sleep(Duration::from_millis(JENKINS_POLLING_INTERVAL));
continue;
}
};
if resp.status().is_server_error() {
sleep(Duration::from_millis(JENKINS_POLLING_INTERVAL));
continue;
}
resp.read_to_string(&mut result_str)
.unwrap_or_else(|err| panic!("Couldn't read from server: {}", err));
break;
}
serde_json::from_str(&result_str)
.unwrap_or_else(|err| panic!("Couldn't parse JSON from Jenkins: {}", err))
}
pub fn get_build_url(&self, build_queue_entry: &str) -> Option<String> {
loop {
let entry = self.get_api_json_object(build_queue_entry);
match entry.get("executable") {
Some(exec) => {
return Some(
exec
.as_object() .unwrap() .get("url") .unwrap() .as_str()
.unwrap()
.to_string(),
);
}
None => sleep(Duration::from_millis(JENKINS_POLLING_INTERVAL)),
}
}
}
pub fn get_build_status(&self, build_url: &str) -> JenkinsBuildStatus {
if self.get_api_json_object(build_url)["building"]
.as_bool()
.unwrap()
{
JenkinsBuildStatus::Running
} else {
JenkinsBuildStatus::Done
}
}
pub fn get_build_result(&self, build_url: &str) -> Option<TestState> {
match self
.get_api_json_object(build_url)
.get("result")
.unwrap()
.as_str()
{
None => None,
Some(result) => match result {
"SUCCESS" => Some(TestState::Success),
"FAILURE" => Some(TestState::Fail),
"UNSTABLE" => Some(TestState::Warning),
_ => Some(TestState::Pending),
},
}
}
pub fn get_results_url(&self, build_url: &str, job: &BTreeMap<String, String>) -> String {
match job.get("artifact") {
Some(artifact) => format!("{}/artifact/{}", build_url, artifact),
None => format!("{}/", build_url),
}
}
pub fn wait_build(&self, build_url: &str) -> JenkinsBuildStatus {
while self.get_build_status(build_url) != JenkinsBuildStatus::Done {
sleep(Duration::from_millis(JENKINS_POLLING_INTERVAL));
}
JenkinsBuildStatus::Done
}
}