rebuilderd_common/
api.rs

1use crate::config::ConfigFile;
2use crate::errors::*;
3use crate::{auth, http, PkgArtifact, PkgGroup, PkgRelease, PublicKeys, Status};
4use chrono::prelude::*;
5use serde::{Deserialize, Serialize};
6use std::borrow::Cow;
7use std::collections::HashMap;
8use std::env;
9use url::Url;
10
11pub const AUTH_COOKIE_HEADER: &str = "X-Auth-Cookie";
12pub const WORKER_KEY_HEADER: &str = "X-Worker-Key";
13pub const SIGNUP_SECRET_HEADER: &str = "X-Signup-Secret";
14
15pub struct Client {
16    endpoint: Url,
17    client: http::Client,
18    is_default_endpoint: bool,
19    auth_cookie: Option<String>,
20    worker_key: Option<String>,
21    signup_secret: Option<String>,
22}
23
24impl Client {
25    pub fn new(config: ConfigFile, endpoint: Option<String>) -> Result<Client> {
26        let (endpoint, auth_cookie, is_default_endpoint) = if let Some(endpoint) = endpoint {
27            let cookie = config
28                .endpoints
29                .get(&endpoint)
30                .map(|e| e.cookie.to_string());
31            (endpoint, cookie, false)
32        } else if let Some(endpoint) = config.http.endpoint {
33            (endpoint, None, true)
34        } else {
35            ("http://127.0.0.1:8484".to_string(), None, true)
36        };
37
38        let mut endpoint = endpoint
39            .parse::<Url>()
40            .with_context(|| anyhow!("Failed to parse endpoint as url: {:?}", endpoint))?;
41
42        // If the url ends with a slash, remove it
43        endpoint
44            .path_segments_mut()
45            .map_err(|_| anyhow!("Given endpoint url cannot be base"))?
46            .pop_if_empty();
47
48        debug!("Setting rebuilderd endpoint to {:?}", endpoint.as_str());
49        let client = http::client()?;
50        Ok(Client {
51            endpoint,
52            client,
53            is_default_endpoint,
54            auth_cookie,
55            worker_key: None,
56            signup_secret: None,
57        })
58    }
59
60    pub fn with_auth_cookie(&mut self) -> Result<&mut Self> {
61        if let Ok(cookie_path) = env::var("REBUILDERD_COOKIE_PATH") {
62            debug!("Found cookie path in environment: {:?}", cookie_path);
63            let auth_cookie =
64                auth::read_cookie_from_file(cookie_path).context("Failed to load auth cookie")?;
65            Ok(self.auth_cookie(auth_cookie))
66        } else if self.is_default_endpoint {
67            let auth_cookie = auth::find_auth_cookie().context("Failed to load auth cookie")?;
68            Ok(self.auth_cookie(auth_cookie))
69        } else {
70            Ok(self)
71        }
72    }
73
74    pub fn auth_cookie<I: Into<String>>(&mut self, cookie: I) -> &mut Self {
75        self.auth_cookie = Some(cookie.into());
76        self
77    }
78
79    pub fn worker_key<I: Into<String>>(&mut self, key: I) {
80        self.worker_key = Some(key.into());
81    }
82
83    pub fn signup_secret<I: Into<String>>(&mut self, secret: I) {
84        self.signup_secret = Some(secret.into());
85    }
86
87    fn url_join(&self, route: &str) -> Url {
88        let mut url = self.endpoint.clone();
89        {
90            // this unwrap is safe because we've called path_segments_mut in the constructor before
91            let mut path = url.path_segments_mut().expect("Url cannot be base");
92            for segment in route.split('/') {
93                path.push(segment);
94            }
95        }
96        url
97    }
98
99    pub fn get(&self, path: Cow<'static, str>) -> http::RequestBuilder {
100        let mut req = self.client.get(self.url_join(&path));
101        if let Some(auth_cookie) = &self.auth_cookie {
102            req = req.header(AUTH_COOKIE_HEADER, auth_cookie);
103        }
104        if let Some(worker_key) = &self.worker_key {
105            req = req.header(WORKER_KEY_HEADER, worker_key);
106        }
107        if let Some(signup_secret) = &self.signup_secret {
108            req = req.header(SIGNUP_SECRET_HEADER, signup_secret);
109        }
110        req
111    }
112
113    pub fn post(&self, path: Cow<'static, str>) -> http::RequestBuilder {
114        let mut req = self.client.post(self.url_join(&path));
115        if let Some(auth_cookie) = &self.auth_cookie {
116            req = req.header(AUTH_COOKIE_HEADER, auth_cookie);
117        }
118        if let Some(worker_key) = &self.worker_key {
119            req = req.header(WORKER_KEY_HEADER, worker_key);
120        }
121        if let Some(signup_secret) = &self.signup_secret {
122            req = req.header(SIGNUP_SECRET_HEADER, signup_secret);
123        }
124        req
125    }
126
127    pub async fn list_workers(&self) -> Result<Vec<Worker>> {
128        let workers = self
129            .get(Cow::Borrowed("api/v0/workers"))
130            .send()
131            .await?
132            .error_for_status()?
133            .json()
134            .await?;
135
136        Ok(workers)
137    }
138
139    pub async fn sync_suite(&self, import: &SuiteImport) -> Result<()> {
140        self.post(Cow::Borrowed("api/v0/pkgs/sync"))
141            .json(import)
142            .send()
143            .await?
144            .error_for_status()?;
145        Ok(())
146    }
147
148    pub async fn list_pkgs(&self, list: &ListPkgs) -> Result<Vec<PkgRelease>> {
149        let pkgs = self
150            .get(Cow::Borrowed("api/v0/pkgs/list"))
151            .query(list)
152            .send()
153            .await?
154            .error_for_status()?
155            .json()
156            .await?;
157        Ok(pkgs)
158    }
159
160    pub async fn match_one_pkg(&self, list: &ListPkgs) -> Result<PkgRelease> {
161        let pkgs = self.list_pkgs(list).await?;
162
163        if pkgs.len() > 1 {
164            bail!("Filter matched too many packages: {}", pkgs.len());
165        }
166
167        let pkg = pkgs
168            .into_iter()
169            .next()
170            .context("Filter didn't match any packages on this rebuilder")?;
171
172        Ok(pkg)
173    }
174
175    pub async fn fetch_log(&self, id: i32) -> Result<Vec<u8>> {
176        let log = self
177            .get(Cow::Owned(format!("api/v0/builds/{}/log", id)))
178            .send()
179            .await?
180            .error_for_status()?
181            .bytes()
182            .await?;
183        Ok(log.to_vec())
184    }
185
186    pub async fn fetch_diffoscope(&self, id: i32) -> Result<Vec<u8>> {
187        let log = self
188            .get(Cow::Owned(format!("api/v0/builds/{}/diffoscope", id)))
189            .send()
190            .await?
191            .error_for_status()?
192            .bytes()
193            .await?;
194        Ok(log.to_vec())
195    }
196
197    pub async fn fetch_attestation(&self, id: i32) -> Result<Vec<u8>> {
198        let attestation = self
199            .get(Cow::Owned(format!("api/v0/builds/{}/attestation", id)))
200            .send()
201            .await?
202            .error_for_status()?
203            .bytes()
204            .await?;
205        Ok(attestation.to_vec())
206    }
207
208    pub async fn fetch_public_keys(&self) -> Result<PublicKeys> {
209        let keys = self
210            .get(Cow::Borrowed("api/v0/public-keys"))
211            .send()
212            .await?
213            .error_for_status()?
214            .json()
215            .await?;
216        Ok(keys)
217    }
218
219    pub async fn list_queue(&self, list: &ListQueue) -> Result<QueueList> {
220        let pkgs = self
221            .post(Cow::Borrowed("api/v0/queue/list"))
222            .json(list)
223            .send()
224            .await?
225            .error_for_status()?
226            .json()
227            .await?;
228        Ok(pkgs)
229    }
230
231    pub async fn push_queue(&self, push: &PushQueue) -> Result<()> {
232        self.post(Cow::Borrowed("api/v0/queue/push"))
233            .json(push)
234            .send()
235            .await?
236            .error_for_status()?
237            .json()
238            .await
239            .map_err(Error::from)
240    }
241
242    pub async fn pop_queue(&self, query: &WorkQuery) -> Result<JobAssignment> {
243        let assignment = self
244            .post(Cow::Borrowed("api/v0/queue/pop"))
245            .json(query)
246            .send()
247            .await?
248            .error_for_status()?
249            .json()
250            .await?;
251        Ok(assignment)
252    }
253
254    pub async fn drop_queue(&self, query: &DropQueueItem) -> Result<()> {
255        self.post(Cow::Borrowed("api/v0/queue/drop"))
256            .json(query)
257            .send()
258            .await?
259            .error_for_status()?
260            .json()
261            .await
262            .map_err(Error::from)
263    }
264
265    pub async fn requeue_pkgs(&self, requeue: &RequeueQuery) -> Result<()> {
266        self.post(Cow::Borrowed("api/v0/pkg/requeue"))
267            .json(requeue)
268            .send()
269            .await?
270            .error_for_status()?
271            .json()
272            .await
273            .map_err(Error::from)
274    }
275
276    pub async fn ping_build(&self, body: &PingRequest) -> Result<()> {
277        self.post(Cow::Borrowed("api/v0/build/ping"))
278            .json(body)
279            .send()
280            .await?
281            .error_for_status()?;
282        Ok(())
283    }
284
285    pub async fn report_build(&self, ticket: &BuildReport) -> Result<()> {
286        self.post(Cow::Borrowed("api/v0/build/report"))
287            .json(ticket)
288            .send()
289            .await?
290            .error_for_status()?;
291        Ok(())
292    }
293}
294
295#[derive(Debug, Serialize, Deserialize)]
296pub enum Success {
297    Ok,
298}
299
300#[derive(Debug, Serialize, Deserialize)]
301pub struct Worker {
302    pub key: String,
303    pub addr: String,
304    pub status: Option<String>,
305    pub last_ping: NaiveDateTime,
306    pub online: bool,
307}
308
309#[derive(Debug, Serialize, Deserialize)]
310pub struct WorkQuery {
311    pub supported_backends: Vec<String>,
312}
313
314#[derive(Debug, PartialEq, Eq, Serialize, Deserialize)]
315pub enum JobAssignment {
316    Nothing,
317    Rebuild(Box<QueueItem>),
318}
319
320#[derive(Debug, Serialize, Deserialize)]
321pub struct SuiteImport {
322    pub distro: String,
323    pub suite: String,
324    pub groups: Vec<PkgGroup>,
325}
326
327#[derive(Debug, Serialize, Deserialize)]
328pub struct ListPkgs {
329    pub name: Option<String>,
330    pub status: Option<Status>,
331    pub distro: Option<String>,
332    pub suite: Option<String>,
333    pub architecture: Option<String>,
334}
335
336#[derive(Debug, Serialize, Deserialize)]
337pub struct QueueList {
338    pub now: NaiveDateTime,
339    pub queue: Vec<QueueItem>,
340}
341
342#[derive(Debug, PartialEq, Eq, Serialize, Deserialize)]
343pub struct QueueItem {
344    pub id: i32,
345    pub pkgbase: PkgGroup,
346    pub version: String,
347    pub queued_at: NaiveDateTime,
348    pub worker_id: Option<i32>,
349    pub started_at: Option<NaiveDateTime>,
350    pub last_ping: Option<NaiveDateTime>,
351}
352
353#[derive(Debug, Serialize, Deserialize)]
354pub struct ListQueue {
355    pub limit: Option<i64>,
356}
357
358#[derive(Debug, Serialize, Deserialize)]
359pub struct PushQueue {
360    pub name: String,
361    pub version: Option<String>,
362    pub priority: i32,
363    pub distro: String,
364    pub suite: String,
365    pub architecture: Option<String>,
366}
367
368#[derive(Debug, Serialize, Deserialize)]
369pub struct DropQueueItem {
370    pub name: String,
371    pub version: Option<String>,
372    pub distro: String,
373    pub suite: String,
374    pub architecture: Option<String>,
375}
376
377#[derive(Debug, Serialize, Deserialize)]
378pub struct RequeueQuery {
379    pub name: Option<String>,
380    pub status: Option<Status>,
381    pub priority: i32,
382    pub distro: Option<String>,
383    pub suite: Option<String>,
384    pub architecture: Option<String>,
385    pub reset: bool,
386}
387
388#[derive(Debug, PartialEq, Eq, Serialize, Deserialize)]
389pub enum BuildStatus {
390    Good,
391    Bad,
392    Fail,
393}
394
395#[derive(Debug, Serialize, Deserialize)]
396pub struct Rebuild {
397    pub status: BuildStatus,
398    pub diffoscope: Option<String>,
399    pub attestation: Option<String>,
400}
401
402impl Rebuild {
403    pub fn new(status: BuildStatus) -> Rebuild {
404        Rebuild {
405            status,
406            diffoscope: None,
407            attestation: None,
408        }
409    }
410}
411
412#[derive(Debug, Serialize, Deserialize)]
413pub struct BuildReport {
414    pub queue: QueueItem,
415    pub build_log: String,
416    pub rebuilds: Vec<(PkgArtifact, Rebuild)>,
417}
418
419#[derive(Debug, Serialize, Deserialize)]
420pub struct DashboardResponse {
421    pub suites: HashMap<String, SuiteStats>,
422    pub active_builds: Vec<QueueItem>,
423    pub queue_length: usize,
424    pub now: NaiveDateTime,
425}
426
427#[derive(Debug, Default, Serialize, Deserialize)]
428pub struct SuiteStats {
429    pub good: usize,
430    pub unknown: usize,
431    pub bad: usize,
432}
433
434#[derive(Debug, Default, Serialize, Deserialize)]
435pub struct PingRequest {
436    pub queue_id: i32,
437}
438
439#[cfg(test)]
440mod tests {
441    use super::*;
442
443    #[test]
444    fn test_endpoint_format_default() {
445        let client = Client::new(ConfigFile::default(), None).unwrap();
446        assert_eq!(client.endpoint, "http://127.0.0.1:8484".parse().unwrap());
447    }
448
449    #[test]
450    fn test_endpoint_format_example_com() {
451        let client =
452            Client::new(ConfigFile::default(), Some("https://example.com".into())).unwrap();
453        assert_eq!(client.endpoint, "https://example.com".parse().unwrap());
454    }
455
456    #[test]
457    fn test_endpoint_format_example_com_trailing_slash() {
458        let client =
459            Client::new(ConfigFile::default(), Some("https://example.com/".into())).unwrap();
460        assert_eq!(client.endpoint, "https://example.com".parse().unwrap());
461    }
462
463    #[test]
464    fn test_endpoint_format_example_com_with_path() {
465        let client = Client::new(
466            ConfigFile::default(),
467            Some("https://example.com/re/build".into()),
468        )
469        .unwrap();
470        assert_eq!(
471            client.endpoint,
472            "https://example.com/re/build".parse().unwrap()
473        );
474    }
475
476    #[test]
477    fn test_endpoint_format_example_com_with_path_trailing_slash() {
478        let client = Client::new(
479            ConfigFile::default(),
480            Some("https://example.com/re/build/".into()),
481        )
482        .unwrap();
483        assert_eq!(
484            client.endpoint,
485            "https://example.com/re/build".parse().unwrap()
486        );
487    }
488}