1use std::{collections::HashMap, time::Duration};
2
3use anyhow::{bail, Context, Result};
4use log::{error, info, trace, warn};
5use reqwest::RequestBuilder;
6use serde::Deserialize;
7use tokio::time::sleep;
8
9#[derive(thiserror::Error, Debug)]
10pub enum Error {
11 #[error("API error: {0}")]
12 APIError(String),
13 #[error("Queue item not exists, maybe already running or finished")]
14 QueueItemNotExists,
15 #[error("Network error: {0}")]
16 NetworkError(reqwest::Error),
17}
18
19pub struct Jenkins {
22 hc: reqwest::Client,
23 url: String,
24 user: String,
25 password: String,
26}
27
28impl Jenkins {
29 pub fn new(url: &str, user: &str, password: &str) -> Jenkins {
36 let hc = reqwest::Client::builder()
37 .connect_timeout(Duration::from_secs(3))
38 .build()
39 .expect("failed to init http client");
40 Jenkins {
41 hc,
42 url: url.to_owned(),
43 user: user.to_owned(),
44 password: password.to_owned(),
45 }
46 }
47
48 pub fn get_url(&self) -> &str {
49 &self.url
50 }
51
52 fn post(&self, url: &str) -> RequestBuilder {
53 self.hc
54 .post(url)
55 .basic_auth(&self.user, Some(&self.password))
56 }
57
58 fn get(&self, url: &str) -> RequestBuilder {
59 self.hc
60 .get(url)
61 .basic_auth(&self.user, Some(&self.password))
62 }
63
64 pub async fn poll_queue_item(
73 &self,
74 queue_item_url: &str,
75 ) -> Result<QueueItemRes, anyhow::Error> {
76 let queue_url = format!("{}api/json", queue_item_url);
77 loop {
78 sleep(Duration::from_secs(3)).await;
79 match self.get(&queue_url).send().await {
80 Ok(queue_res) => {
81 info!("queue_res={:?}", queue_res);
82 if queue_res.status().is_client_error() {
83 bail!(Error::QueueItemNotExists)
84 }
85 let qi_res: QueueItemRes = queue_res
86 .json()
87 .await
88 .context("parse queue item payload as json")?;
89 if qi_res.executable.is_some() {
90 info!("Get {}: body={:?}", queue_url, qi_res);
91 return Ok(qi_res);
92 } else {
93 trace!("Get {}: body={:?}", queue_url, qi_res);
94 }
95 }
96 Err(err) => {
97 error!("Get {}: err={:?}", queue_url, err);
98 bail!(Error::NetworkError(err));
100 }
101 }
102 }
103 }
104
105 pub async fn build_with_parameter(
113 &self,
114 job: &str,
115 params: HashMap<&str, &str>,
116 ) -> Result<QueueItemRes> {
117 let url = format!("{}/job/{}/buildWithParameters", self.url, job);
118 match self.post(&url).form(¶ms).send().await {
119 Ok(res) => {
120 if res.status().is_success() {
121 info!("buildWithParameters - job={}, res={:?}", job, res);
122 if let Some(location) = res.headers().get("location") {
123 let queue_url = location.to_str().expect("location header");
124 Ok(self.poll_queue_item(queue_url).await?)
125 } else {
126 bail!(Error::APIError("location header not available".to_owned()))
127 }
128 } else {
129 warn!("buildWithParameters - job={}, res={:?}", job, res);
130 bail!(Error::APIError(format!("http status: {}", res.status())))
131 }
132 }
133 Err(err) => {
134 error!("buildWithParameters - job={}, err={:?}", job, err);
135 bail!(err)
136 }
137 }
138 }
139}
140
141#[derive(Deserialize, Debug)]
142pub struct QueueItemExecutable {
143 pub number: i32,
144 pub url: String,
145}
146#[derive(Deserialize, Debug)]
147pub struct QueueItemRes {
148 pub why: Option<String>,
149 pub executable: Option<QueueItemExecutable>,
150}
151
152#[cfg(test)]
153mod tests {
154 use super::*;
155
156 #[tokio::test]
158 async fn build_with_parameter() {
159 let _ = env_logger::builder().is_test(true).try_init();
160
161 let cli = Jenkins::new(
162 "https://jenkins.domain.com",
163 "jenkins-user",
164 "jenkins-token",
165 );
166 let params = HashMap::from([("HostLimit", "xxx"), ("Module", "ansible.builtin.ping")]);
167 let res = cli
168 .build_with_parameter("ansible-global-adhoc", params)
169 .await;
170 println!("{:?}", res);
171 }
172}