jenkins_rs/
lib.rs

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
19/// [Jenkins : Remote access API](https://wiki.jenkins.io/display/JENKINS/Remote+access+API)
20///
21pub struct Jenkins {
22    hc: reqwest::Client,
23    url: String,
24    user: String,
25    password: String,
26}
27
28impl Jenkins {
29    /// Create Jenkins instance
30    ///
31    /// ## Arguments
32    ///
33    /// * `password` - password or api token of user
34    ///
35    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    /// Poll from new build queue item url until build number available
65    ///
66    /// [reference](https://docs.cloudbees.com/docs/cloudbees-ci-kb/latest/client-and-managed-controllers/get-build-number-with-rest-api)
67    ///
68    /// ## Arguments
69    ///
70    /// * `queue_item_url` - `location` field in `build`/`buildWithParameters` response header
71    ///
72    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                    // Err(err)
99                    bail!(Error::NetworkError(err));
100                }
101            }
102        }
103    }
104
105    /// [Parameterized Build](https://wiki.jenkins.io/display/JENKINS/Parameterized-Build.html)
106    ///
107    /// ## Arguments
108    ///
109    /// * `job` - job name
110    /// * `params` - parameters to trigger a build
111    ///
112    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(&params).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(flavor = "multi_thread", worker_threads = 4)]
157    #[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}