Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ USAGE:
FLAGS:
-h, --help Prints help information
--no-check-certificate Disable TLS certificate check for the Hydra host
--verbose Print verbose diagnostics including HTTP responses
-V, --version Prints version information

OPTIONS:
Expand All @@ -81,6 +82,9 @@ A client to query Hydra through its JSON API.
The `project-create` command creates a new project under the name specified. The created project
will be _enabled_ and _visible_. _Note_: this command requires user authentication.

Use `--verbose` to print diagnostic output, including full HTTP status, headers, and body for each
server response during the command.

`$ hydra-cli project-create --help`
```
hydra-cli-project-create
Expand Down
108 changes: 85 additions & 23 deletions hydra-cli/src/hydra/reqwest_client.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
use crate::hydra::client::*;

use reqwest::blocking::Client as ReqwestClient;
use reqwest::blocking::Response;
use reqwest::header::{CONTENT_TYPE, REFERER};
use serde::de::DeserializeOwned;
use serde_json::Value;
Expand All @@ -24,30 +25,86 @@ impl From<reqwest::Error> for ClientError {
pub struct Client {
pub host: String,
pub client: ReqwestClient,
pub verbose: bool,
}

impl Client {
pub fn new(client: ReqwestClient, host: String) -> Client {
Client { client, host }
Client::new_with_verbose(client, host, false)
}

pub fn new_with_verbose(client: ReqwestClient, host: String, verbose: bool) -> Client {
Client {
client,
host,
verbose,
}
}
}

fn log_response(
status: reqwest::StatusCode,
headers: &[(String, String)],
body: &str,
) {
println!("< HTTP {}", status);
for (header_name, header_value) in headers.iter() {
println!(
"< {}: {}",
header_name,
header_value
);
}
println!("<");
print!("{}", body);
if !body.ends_with('\n') {
println!();
}
}

/// Performs a GET request retrieving a deserializable response
fn get_json<T: DeserializeOwned>(client: &ReqwestClient, url: &str) -> Result<T, ClientError> {
fn get_json<T: DeserializeOwned>(
client: &ReqwestClient,
url: &str,
verbose: bool,
) -> Result<T, ClientError> {
let res = client
.get(url)
.header(reqwest::header::CONTENT_TYPE, "application/json")
.send()?;
let (status, body) = read_response_body(res, verbose)?;

if res.status().is_success() {
let v: Value = res.json()?;
if status.is_success() {
let v: Value = serde_json::from_str(&body)
.map_err(|e| ClientError::Error(format!("error decoding response body: {}", e)))?;
match serde_json::from_value(v) {
Ok(x) => Ok(x),
Err(x) => Err(ClientError::InvalidResponse(format!("{}", x))),
}
} else {
Err(ClientError::Error(format!("{}", res.status())))
Err(ClientError::Error(format!("{}", status)))
}
}

fn read_response_body(res: Response, verbose: bool) -> Result<(reqwest::StatusCode, String), ClientError> {
let status = res.status();
let headers = res
.headers()
.iter()
.map(|(name, value)| {
(
name.as_str().to_string(),
value.to_str().unwrap_or("<non-utf8>").to_string(),
)
})
.collect::<Vec<(String, String)>>();
let body = res.text()?;

if verbose {
log_response(status, &headers, &body);
}

Ok((status, body))
}

impl HydraClient for Client {
Expand All @@ -64,39 +121,40 @@ impl HydraClient for Client {
.header(REFERER, self.host.as_str())
.json(&proj)
.send()?;
let (status, _) = read_response_body(res, self.verbose)?;

if res.status().is_success() {
if status.is_success() {
Ok(())
} else {
Err(ClientError::Error(format!("{}", res.status())))
Err(ClientError::Error(format!("{}", status)))
}
}

fn host(&self) -> String {
self.host.clone()
}
fn projects(&self) -> Result<Vec<Project>, ClientError> {
get_json(&self.client, &self.host)
get_json(&self.client, &self.host, self.verbose)
}

fn search(&self, query: &str) -> Result<Search, ClientError> {
let request_url = format!("{}/search?query={}", &self.host, query);
get_json(&self.client, &request_url)
get_json(&self.client, &request_url, self.verbose)
}

fn jobset_overview(&self, project: &str) -> Result<Vec<JobsetOverview>, ClientError> {
let request_url = format!("{}/api/jobsets?project={}", &self.host, project);
get_json(&self.client, &request_url)
get_json(&self.client, &request_url, self.verbose)
}

fn jobset(&self, project: &str, jobset: &str) -> Result<Jobset, ClientError> {
let request_url = format!("{}/jobset/{}/{}", &self.host, project, jobset);
get_json(&self.client, &request_url)
get_json(&self.client, &request_url, self.verbose)
}

fn eval(&self, number: i64) -> Result<Eval, ClientError> {
let request_url = format!("{}/eval/{}", &self.host, number);
get_json(&self.client, &request_url)
get_json(&self.client, &request_url, self.verbose)
}

fn jobset_create(
Expand All @@ -112,11 +170,12 @@ impl HydraClient for Client {
.header(REFERER, self.host.as_str())
.json(&jobset_config)
.send()?;
let (status, _) = read_response_body(res, self.verbose)?;

if res.status().is_success() {
if status.is_success() {
Ok(())
} else {
Err(ClientError::Error(format!("{}", res.status())))
Err(ClientError::Error(format!("{}", status)))
}
}

Expand All @@ -128,11 +187,12 @@ impl HydraClient for Client {
.header(REFERER, self.host.as_str())
.header(CONTENT_TYPE, "application/json")
.send()?;
let (status, _) = read_response_body(res, self.verbose)?;

if res.status().is_success() {
if status.is_success() {
Ok(())
} else {
Err(ClientError::Error(format!("{}", res.status())))
Err(ClientError::Error(format!("{}", status)))
}
}

Expand All @@ -146,11 +206,12 @@ impl HydraClient for Client {
.put(&request_url)
.header(REFERER, self.host.as_str())
.send()?;
let (status, _) = read_response_body(res, self.verbose)?;

if res.status().is_success() {
if status.is_success() {
Ok(())
} else {
Err(ClientError::Error(format!("{}", res.status())))
Err(ClientError::Error(format!("{}", status)))
}
}

Expand All @@ -165,12 +226,13 @@ impl HydraClient for Client {

match login_res {
Ok(r) => {
if r.status().is_success() {
let (status, _) = read_response_body(r, self.verbose)?;
if status.is_success() {
Ok(())
} else if r.status().is_redirection() {
} else if status.is_redirection() {
Ok(())
} else {
Err(ClientError::Error(format!("Response Error: {}", r.status())))
Err(ClientError::Error(format!("Response Error: {}", status)))
}
}
Err(err) => Err(ClientError::Error(format!("Request Error: {}", err))),
Expand Down Expand Up @@ -202,7 +264,7 @@ mod tests {
.create();

let c = client();
let res: Result<Project, ClientError> = get_json(&c.client, &c.host);
let res: Result<Project, ClientError> = get_json(&c.client, &c.host, false);
assert_eq!(
res,
Err(ClientError::Error("500 Internal Server Error".to_string()))
Expand All @@ -218,7 +280,7 @@ mod tests {
.create();

let c = client();
let res: Result<Vec<Project>, ClientError> = get_json(&c.client, &c.host);
let res: Result<Vec<Project>, ClientError> = get_json(&c.client, &c.host, false);

assert_eq!(
res,
Expand Down
8 changes: 7 additions & 1 deletion hydra-cli/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,11 @@ fn main() {
.long("no-check-certificate")
.help("Disable TLS certificate check for the Hydra host"),
)
.arg(
Arg::with_name("verbose")
.long("verbose")
.help("Print verbose diagnostics including HTTP responses"),
)
.subcommand(
SubCommand::with_name("search")
.about("Search by output paths")
Expand Down Expand Up @@ -208,6 +213,7 @@ fn main() {
let matches = app.get_matches();
let host = matches.value_of("host").unwrap();
let no_check_certs = matches.is_present("no-check-certificate");
let verbose = matches.is_present("verbose");

let custom = redirect::Policy::none();

Expand All @@ -217,7 +223,7 @@ fn main() {
.redirect(custom)
.build()
.unwrap();
let client = ReqwestHydraClient::new(c, String::from(host));
let client = ReqwestHydraClient::new_with_verbose(c, String::from(host), verbose);

let cmd_res: OpResult = match matches.subcommand() {
("search", Some(args)) => search::run(
Expand Down