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 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 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}