tcfetch/
taskcluster.rs

1use crate::utils::{get_json, url};
2use crate::{Result, TaskFilter};
3use reqwest;
4use serde_derive::Deserialize;
5use std::collections::BTreeMap;
6
7pub(crate) trait TaskclusterCI {
8    fn default_artifact_name(&self) -> &'static str;
9    fn default_task_filter(&self) -> Vec<TaskFilter>;
10    fn get_taskgroups(
11        &self,
12        client: &reqwest::blocking::Client,
13        commit: &str,
14    ) -> Result<Vec<String>>;
15    fn taskcluster(&self) -> &Taskcluster;
16}
17
18#[derive(Debug, PartialEq, Deserialize)]
19#[serde(rename_all = "lowercase")]
20pub enum TaskState {
21    Unscheduled,
22    Pending,
23    Running,
24    Completed,
25    Failed,
26    Exception,
27}
28
29impl TaskState {
30    pub fn is_complete(&self) -> bool {
31        match self {
32            TaskState::Unscheduled | TaskState::Pending | TaskState::Running => false,
33            TaskState::Completed | TaskState::Failed | TaskState::Exception => true,
34        }
35    }
36}
37
38#[derive(Debug, Deserialize)]
39#[allow(non_snake_case)]
40pub struct IndexResponse {
41    pub namespace: String,
42    pub taskId: String,
43    pub rank: u64,
44    pub expires: String,
45}
46
47#[derive(Debug, Deserialize)]
48#[allow(non_snake_case)]
49pub struct ArtifactsResponse {
50    pub artifacts: Vec<Artifact>,
51}
52
53#[derive(Debug, Deserialize)]
54#[allow(non_snake_case)]
55pub struct Artifact {
56    pub storageType: String,
57    pub name: String,
58    pub expires: String,
59    pub contentType: String,
60}
61
62#[derive(Debug, Deserialize)]
63#[allow(non_snake_case)]
64pub struct TaskGroupResponse {
65    pub taskGroupId: String,
66    pub tasks: Vec<TaskGroupTask>,
67    pub continuationToken: Option<String>,
68}
69
70#[derive(Debug, Deserialize)]
71#[allow(non_snake_case)]
72pub struct TaskGroupTask {
73    pub status: TaskGroupTaskStatus,
74    pub task: Task,
75}
76
77#[derive(Debug, Deserialize)]
78#[allow(non_snake_case)]
79pub struct TaskGroupTaskStatus {
80    pub taskId: String,
81    pub provisionerId: String,
82    pub workerType: String,
83    pub schedulerId: String,
84    pub taskGroupId: String,
85    pub deadline: String, // Should be a time type
86    pub expires: String,  // Should be a time type
87    pub retriesLeft: u64,
88    pub state: TaskState,
89    pub runs: Vec<TaskRun>,
90}
91
92#[derive(Debug, Deserialize)]
93#[allow(non_snake_case)]
94pub struct TaskRun {
95    pub runId: u64,
96    pub state: TaskState,
97    pub reasonCreated: String,          // Should be an enum
98    pub reasonResolved: Option<String>, // Should be an enum
99    pub workerGroup: Option<String>,
100    pub workerId: Option<String>,
101    pub takenUntil: Option<String>, // Should be a time type
102    pub scheduled: Option<String>,  // Should be a time type
103    pub started: Option<String>,    // Should be a time type
104    pub resolved: Option<String>,   // Should be a time type
105}
106
107#[derive(Debug, Deserialize)]
108#[allow(non_snake_case)]
109pub struct Task {
110    pub provisionerId: String,
111    pub workerType: String,
112    pub schedulerId: String,
113    pub taskGroupId: String,
114    pub metadata: TaskMetadata,
115    #[serde(default)]
116    pub extra: BTreeMap<String, serde_json::Value>,
117}
118
119#[derive(Debug, Deserialize)]
120#[allow(non_snake_case)]
121pub struct TaskMetadata {
122    pub owner: String,
123    pub source: String,
124    pub description: String,
125    pub name: String,
126}
127
128pub struct Taskcluster {
129    pub index_base: String,
130    pub queue_base: String,
131}
132
133impl Taskcluster {
134    pub fn new(taskcluster_base: &str) -> Taskcluster {
135        if taskcluster_base == "https://taskcluster.net" {
136            Taskcluster {
137                index_base: "https://index.taskcluster.net/v1/".into(),
138                queue_base: "https://queue.taskcluster.net/v1/".into(),
139            }
140        } else {
141            Taskcluster {
142                index_base: format!("{}/api/index/v1/", taskcluster_base),
143                queue_base: format!("{}/api/queue/v1/", taskcluster_base),
144            }
145        }
146    }
147
148    pub fn get_taskgroup_tasks(
149        &self,
150        client: &reqwest::blocking::Client,
151        taskgroup_id: &str,
152    ) -> Result<Vec<TaskGroupTask>> {
153        let url_suffix = format!("task-group/{}/list", taskgroup_id);
154        let mut tasks = Vec::new();
155        let mut continuation_token: Option<String> = None;
156        loop {
157            let query = continuation_token.map(|token| vec![("continuationToken".into(), token)]);
158            let data: TaskGroupResponse =
159                get_json(client, &url(&self.queue_base, &url_suffix), query, None)?;
160            tasks.extend(data.tasks);
161            if data.continuationToken.is_none() {
162                break;
163            }
164            continuation_token = data.continuationToken;
165        }
166        Ok(tasks)
167    }
168
169    pub fn get_artifacts(
170        &self,
171        client: &reqwest::blocking::Client,
172        task_id: &str,
173    ) -> Result<Vec<Artifact>> {
174        let url_suffix = format!("task/{}/artifacts", task_id);
175        let artifacts: ArtifactsResponse =
176            get_json(client, &url(&self.queue_base, &url_suffix), None, None)?;
177        Ok(artifacts.artifacts)
178    }
179
180    pub fn get_log_url(&self, task_id: &str, artifact: &Artifact) -> String {
181        let task_url = format!("task/{}/artifacts", task_id);
182        url(
183            &self.queue_base,
184            &format!("{}/{}", &task_url, artifact.name),
185        )
186    }
187}
188
189pub fn tasks_complete<'a, I>(mut tasks: I) -> bool
190where
191    I: Iterator<Item = &'a TaskGroupTask>,
192{
193    tasks.all(|task| task.status.state.is_complete())
194}