taskcluster/lib.rs
1/*!
2# Taskcluster Client for Rust
3
4For a general guide to using Taskcluster clients, see [Calling Taskcluster
5APIs](https://docs.taskcluster.net/docs/manual/using/api).
6
7This client is a convenience wrapper around `reqwest` that provides named functions for each API
8endpoint and adds functionality such as authentication and retries.
9
10# Usage
11
12## Setup
13
14Before calling an API end-point, you'll need to build a client, using the
15[`ClientBuilder`](crate::ClientBuilder) type. This allows construction of a client with only the
16necessary features, following the builder pattern. You must at least supply a root URL to identify
17the Taskcluster deployment to which the API calls should be directed.
18
19There is a type for each service, e.g., [`Queue`](crate::Queue) and [`Auth`](crate::Auth). Each service type defines functions
20spepcific to the API endpoints for that service. Each has a `new` associated function that
21takes an `Into<ClientBuilder>`. As a shortcut, you may pass a string to `new` that will be treated
22as a root URL.
23
24Here is a simple setup and use of an un-authenticated client:
25
26```
27# use httptest::{matchers::*, responders::*, Expectation, Server};
28# use tokio;
29# use anyhow::Result;
30# use serde_json::json;
31# #[tokio::main]
32# async fn main() -> Result<()> {
33# let server = Server::run();
34# server.expect(
35# Expectation::matching(request::method_path("GET", "/api/auth/v1/clients/static%2Ftaskcluster%2Froot"))
36# .respond_with(
37# status_code(200)
38# .append_header("Content-Type", "application/json")
39# .body("{\"clientId\": \"static/taskcluster/root\"}"))
40# );
41# let root_url = format!("http://{}", server.addr());
42use taskcluster::Auth;
43let auth = Auth::new(root_url)?;
44let resp = auth.client("static/taskcluster/root").await?;
45assert_eq!(resp, json!({"clientId": "static/taskcluster/root"}));
46Ok(())
47# }
48```
49
50Here is an example with credentials provided, in this case via the [standard environment variables](https://docs.taskcluster.net/docs/manual/design/env-vars).
51
52```
53# use httptest::{matchers::*, responders::*, Expectation, Server};
54# use tokio;
55use std::env;
56# use anyhow::Result;
57# #[tokio::main]
58# async fn main() -> Result<()> {
59# let server = Server::run();
60# server.expect(
61# Expectation::matching(request::method_path("POST", "/api/queue/v1/task/G08bnnBuR6yDhDLJkJ6KiA/cancel"))
62# .respond_with(
63# status_code(200)
64# .append_header("Content-Type", "application/json")
65# .body("{\"status\": \"...\"}"))
66# );
67# env::set_var("TASKCLUSTER_ROOT_URL", format!("http://{}", server.addr()));
68# env::set_var("TASKCLUSTER_CLIENT_ID", "a-client");
69# env::set_var("TASKCLUSTER_ACCESS_TOKEN", "a-token");
70use taskcluster::{ClientBuilder, Queue, Credentials};
71let creds = Credentials::from_env()?;
72let root_url = env::var("TASKCLUSTER_ROOT_URL").unwrap();
73let client = Queue::new(ClientBuilder::new(&root_url).credentials(creds))?;
74let res = client.cancelTask("G08bnnBuR6yDhDLJkJ6KiA").await?;
75println!("{}", res.get("status").unwrap());
76Ok(())
77# }
78```
79
80### Authorized Scopes
81
82If you wish to perform requests on behalf of a third-party that has smaller set
83of scopes than you do, you can specify [which scopes your request should be
84allowed to
85use](https://docs.taskcluster.net/docs/manual/design/apis/hawk/authorized-scopes).
86
87These "authorized scopes" are configured on the client:
88
89```
90# use httptest::{matchers::*, responders::*, Expectation, Server};
91# use tokio;
92use std::env;
93use serde_json::json;
94# use anyhow::Result;
95# #[tokio::main]
96# async fn main() -> Result<()> {
97# let server = Server::run();
98# server.expect(
99# Expectation::matching(request::method_path("PUT", "/api/queue/v1/task/G08bnnBuR6yDhDLJkJ6KiA"))
100# .respond_with(
101# status_code(200)
102# .append_header("Content-Type", "application/json")
103# .body("{\"taskId\": \"...\"}"))
104# );
105# env::set_var("TASKCLUSTER_ROOT_URL", format!("http://{}", server.addr()));
106# env::set_var("TASKCLUSTER_CLIENT_ID", "a-client");
107# env::set_var("TASKCLUSTER_ACCESS_TOKEN", "a-token");
108use taskcluster::{ClientBuilder, Queue, Credentials};
109let creds = Credentials::from_env()?;
110let root_url = env::var("TASKCLUSTER_ROOT_URL").unwrap();
111let client = Queue::new(
112 ClientBuilder::new(&root_url)
113 .credentials(creds)
114 .authorized_scopes(vec!["just:one-scope"]))?;
115# let task = json!({});
116let res = client.createTask("G08bnnBuR6yDhDLJkJ6KiA", &task).await?;
117Ok(())
118# }
119```
120
121## Calling API Methods
122
123API methods are available as methods on the corresponding client object. They are capitalized in
124snakeCase (e.g., `createTask`) to match the Taskcluster documentation.
125
126Each method takes arguments in the following order, where appropriate to the method:
127 * Positional arguments (strings interpolated into the URL)
128 * Request body (payload)
129 * URL query arguments
130
131The payload comes in the form of a `serde_json::Value`, the contents of which should match the API
132method's input schema. URL query arguments are all optional.
133
134For example, the following lists all Auth clients:
135
136```
137# // note: pagination is more thoroughly tested in `tests/against_real_deployment.rs`
138# use httptest::{matchers::*, responders::*, Expectation, Server};
139# use tokio;
140# use std::env;
141# use anyhow::Result;
142# #[tokio::main]
143# async fn main() -> Result<()> {
144# let server = Server::run();
145# server.expect(
146# Expectation::matching(request::method_path("GET", "/api/auth/v1/clients/"))
147# .respond_with(
148# status_code(200)
149# .append_header("Content-Type", "application/json")
150# .body("{\"clients\": []}"))
151# );
152# let root_url = format!("http://{}", server.addr());
153use taskcluster::{Auth, ClientBuilder, Credentials};
154let auth = Auth::new(ClientBuilder::new(&root_url))?;
155let mut continuation_token: Option<String> = None;
156let limit = Some("10");
157
158loop {
159 let res = auth
160 .listClients(None, continuation_token.as_deref(), limit)
161 .await?;
162 for client in res.get("clients").unwrap().as_array().unwrap() {
163 println!("{:?}", client);
164 }
165 if let Some(v) = res.get("continuationToken") {
166 continuation_token = Some(v.as_str().unwrap().to_owned());
167 } else {
168 break;
169 }
170}
171# Ok(())
172# }
173```
174
175### Error Handling
176
177All 5xx (server error) responses are automatically retried.
178All 4xx (client error) responses are converted to `Result::Err`.
179All other responses are treated as successful responses.
180Note that this includes 3xx (redirection) responses; the client does not automatically follow such redirects.
181
182Client methods return `anyhow::Error`, but this can be downcast to a `reqwest::Error` if needed.
183As a shortcut for the common case of getting the HTTP status code for an error, use [`err_status_code`](crate::err_status_code).
184The `reqwest::StatusCode` type that this returns is re-exported from this crate.
185
186### Low-Level Access
187
188Instead of using service-specific types, it is possible to call API methods directly by path, using
189the [`Client`](crate::Client) type:
190
191```
192# use httptest::{matchers::*, responders::*, Expectation, Server};
193# use tokio;
194use std::env;
195# use anyhow::Result;
196# #[tokio::main]
197# async fn main() -> Result<()> {
198# let server = Server::run();
199# server.expect(
200# Expectation::matching(request::method_path("POST", "/api/queue/v1/task/G08bnnBuR6yDhDLJkJ6KiA/cancel"))
201# .respond_with(status_code(200))
202# );
203# env::set_var("TASKCLUSTER_ROOT_URL", format!("http://{}", server.addr()));
204# env::set_var("TASKCLUSTER_CLIENT_ID", "a-client");
205# env::set_var("TASKCLUSTER_ACCESS_TOKEN", "a-token");
206use taskcluster::{ClientBuilder, Credentials};
207let creds = Credentials::from_env()?;
208let root_url = env::var("TASKCLUSTER_ROOT_URL").unwrap();
209let client = ClientBuilder::new(&root_url).credentials(creds).build()?;
210let resp = client.request("POST", "api/queue/v1/task/G08bnnBuR6yDhDLJkJ6KiA/cancel", None, None).await?;
211assert!(resp.status().is_success());
212# Ok(())
213# }
214```
215
216## Uploading and Downloading Objects
217
218The [`taskcluster-upload`](https://crates.io/crates/taskcluster-upload) and [`taskcluster-download`](https://crates.io/crates/taskcluster-download) crates contain dedicated support for resilient uploads and downloads to/from the Taskcluster object service.
219This comes in the form of functions that will both interface with the object service API and perform the negotiated upload/download method.
220In all cases, you must supply a pre-configured [`Object`] client, as well as required parameters to the object service API methods.
221
222## Generating URLs
223
224To generate a unsigned URL for an API method, use `<method>_url`:
225
226```
227# use anyhow::Result;
228# fn main() -> Result<()> {
229use taskcluster::{Auth, ClientBuilder};
230# use std::env;
231# env::set_var("TASKCLUSTER_ROOT_URL", "https://tc-tests.example.com");
232let root_url = env::var("TASKCLUSTER_ROOT_URL").unwrap();
233let auth = Auth::new(ClientBuilder::new(&root_url))?;
234let url = auth.listClients_url(Some("static/"), None, None)?;
235assert_eq!(url, "https://tc-tests.example.com/api/auth/v1/clients/?prefix=static%2F".to_owned());
236# Ok(())
237# }
238```
239
240## Generating Temporary Credentials
241
242The [`create_named_temp_creds`](crate::Credentials::create_named_temp_creds) method creates
243temporary credentials:
244
245```
246use std::env;
247use std::time::Duration;
248# use anyhow::Result;
249# fn main() -> Result<()> {
250# env::set_var("TASKCLUSTER_CLIENT_ID", "a-client");
251# env::set_var("TASKCLUSTER_ACCESS_TOKEN", "a-token");
252use taskcluster::Credentials;
253let creds = Credentials::from_env()?;
254let temp_creds = creds.create_named_temp_creds(
255 "new-client-id",
256 Duration::from_secs(3600),
257 vec!["scope1", "scope2"])?;
258assert_eq!(temp_creds.client_id, "new-client-id");
259# Ok(())
260# }
261```
262
263There is also a `create_temp_creds` method which creates unamed temporary credentials, but its use
264is not recommended.
265
266## Generating Timestamps
267
268Taskcluster APIs expects ISO 8601 timestamps, of the sort generated by the JS `Date.toJSON` method.
269The [`chrono`](https://docs.rs/chrono/) crate supports generating compatible timestamps if included with the `serde` feature.
270This crate re-exports `chrono` with that feature enabled.
271To duplicate the functionality of the `fromNow` function from other Taskcluster client libraries, use something like this:
272
273```
274use taskcluster::chrono::{DateTime, Utc, Duration};
275use serde_json::json;
276
277let expires = Utc::now() + Duration::days(2);
278let json = json!({ "expires": expires });
279```
280
281## Generating SlugIDs
282
283Use the [slugid](https://crates.io/crates/slugid) crate to create slugIds (such as for a taskId).
284
285*/
286
287mod client;
288mod credentials;
289mod generated;
290pub mod retry;
291mod util;
292
293// re-export
294pub use chrono;
295
296// internal re-exports
297pub use client::{Client, ClientBuilder};
298pub use credentials::Credentials;
299pub use generated::*;
300pub use reqwest::StatusCode;
301pub use retry::Retry;
302pub use util::err_status_code;