oci_registry_client/
lib.rs

1//! A async client for [OCI compliant image
2//! registries](http://github.com/opencontainers/distribution-spec/blob/master/spec.md/)
3//! and [Docker Registry HTTP V2 protocol](https://docs.docker.com/registry/spec/api/).
4//!
5//! # Usage
6//!
7//! The [`DockerRegistryClientV2`] provides functions to query Registry API and download blobs.
8//!
9//! ```no_run
10//! use std::{path::Path, fs::File, io::Write};
11//! use oci_registry_client::DockerRegistryClientV2;
12//!
13//! # async fn example() -> Result<(), Box<dyn std::error::Error>> {
14//! let mut client = DockerRegistryClientV2::new(
15//!     "registry.docker.io",
16//!     "https://registry-1.docker.io",
17//!     "https://auth.docker.io/token"
18//! );
19//! let token = client.auth("repository", "library/ubuntu", "latest").await?;
20//! client.set_auth_token(Some(token));
21//!
22//! let manifest = client.manifest("library/ubuntu", "latest").await?;
23//! println!("{:?}", manifest);
24//!
25//! for layer in &manifest.layers {
26//!    let mut out_file = File::create(Path::new("/tmp/").join(&layer.digest.to_string()))?;
27//!    let mut blob = client.blob("library/ubuntu", &layer.digest).await?;
28//!
29//!    while let Some(chunk) = blob.chunk().await? {
30//!        out_file.write_all(&chunk)?;
31//!    }
32//! }
33//!
34//! # Ok(())
35//! # }
36//! ```
37
38pub mod blob;
39pub mod errors;
40pub mod manifest;
41
42use blob::Blob;
43use errors::{ErrorList, ErrorResponse};
44use manifest::{Digest, Image, Manifest, ManifestList};
45use reqwest::{Method, StatusCode};
46
47static USER_AGENT: &str = concat!(env!("CARGO_PKG_NAME"), "/", env!("CARGO_PKG_VERSION"));
48
49/// Client to fetch image manifests and download blobs.
50///
51/// DockerRegistryClientV2 provides functions to fetch manifests and download
52/// blobs from a OCI Image Registry (or a Docker Registry API V2).
53#[derive(Clone, Debug)]
54pub struct DockerRegistryClientV2 {
55    service: String,
56    api_url: String,
57    oauth_url: String,
58    auth_token: Option<AuthToken>,
59    client: reqwest::Client,
60}
61
62#[derive(serde::Deserialize, Debug)]
63#[serde(rename_all = "camelCase")]
64pub struct Version {}
65
66const MEDIA_TYPE_JSON: &str = "application/json";
67const MEDIA_TYPE_MANIFEST_LIST_V2: &str =
68    "application/vnd.docker.distribution.manifest.list.v2+json";
69const MEDIA_TYPE_MANIFEST_V2: &str = "application/vnd.docker.distribution.manifest.v2+json";
70const MEDIA_TYPE_IMAGE_CONFIG: &str = "application/vnd.docker.container.image.v1+json";
71
72impl DockerRegistryClientV2 {
73    /// Returns a new `DockerRegistryClientV2`.
74    ///
75    /// # Arguments
76    ///
77    /// * `service` - Name of a Image Registry Service (example: registry.docker.io)
78    /// * `api_url` - Service HTTPS address (example: https://registry-1.docker.io)
79    /// * `auth_url` - Address to get a OAuth 2.0 token for this service.
80    ///
81    /// # Example
82    ///
83    /// ```no_run
84    /// # use oci_registry_client::DockerRegistryClientV2;
85    /// let mut client = DockerRegistryClientV2::new(
86    ///     "registry.docker.io",
87    ///     "https://registry-1.docker.io",
88    ///     "https://auth.docker.io/token"
89    /// );
90    /// ```
91    pub fn new<T: Into<String>>(service: T, api_url: T, oauth_url: T) -> Self {
92        let client = reqwest::Client::builder()
93            .user_agent(USER_AGENT)
94            .build()
95            .unwrap();
96
97        Self {
98            service: service.into(),
99            api_url: api_url.into(),
100            oauth_url: oauth_url.into(),
101            auth_token: None,
102            client,
103        }
104    }
105
106    /// Set access token to authenticate subsequent requests.
107    pub fn set_auth_token(&mut self, token: Option<AuthToken>) {
108        self.auth_token = token;
109    }
110
111    /// Fetch a access token from `auth_url` for this `service`.
112    ///
113    /// # Arguments
114    ///
115    /// * `type` - Scope type (example: "repository").
116    /// * `name` - Name of resource (example: "library/ubuntu").
117    /// * `action` - List of actions separated by comma (example: "pull").
118    pub async fn auth(
119        &self,
120        r#type: &str,
121        name: &str,
122        action: &str,
123    ) -> Result<AuthToken, ErrorResponse> {
124        let response = self
125            .client
126            .get(&self.oauth_url)
127            .query(&[
128                ("service", self.service.clone()),
129                ("scope", format!("{}:{}:{}", r#type, name, action)),
130            ])
131            .send()
132            .await?;
133
134        match response.status() {
135            StatusCode::OK => Ok(response.json::<AuthToken>().await?),
136            _ => Err(ErrorResponse::APIError(response.json::<ErrorList>().await?)),
137        }
138    }
139
140    /// Get API version.
141    pub async fn version(&self) -> Result<Version, ErrorResponse> {
142        let url = format!("{}/v2", self.api_url);
143        self.request(Method::GET, &url, MEDIA_TYPE_JSON).await
144    }
145
146    /// List manifests from given image and reference.
147    pub async fn list_manifests(
148        &self,
149        image: &str,
150        reference: &str,
151    ) -> Result<ManifestList, ErrorResponse> {
152        let url = format!("{}/v2/{}/manifests/{}", &self.api_url, image, reference);
153        self.request(Method::GET, &url, MEDIA_TYPE_MANIFEST_LIST_V2)
154            .await
155    }
156
157    /// Get the image manifest.
158    pub async fn manifest(&self, image: &str, reference: &str) -> Result<Manifest, ErrorResponse> {
159        let url = format!("{}/v2/{}/manifests/{}", &self.api_url, image, reference);
160        self.request(Method::GET, &url, MEDIA_TYPE_MANIFEST_V2)
161            .await
162    }
163
164    /// Get the container config.
165    pub async fn config(&self, image: &str, reference: &Digest) -> Result<Image, ErrorResponse> {
166        let url = format!("{}/v2/{}/blobs/{}", &self.api_url, image, reference);
167        self.request(Method::GET, &url, MEDIA_TYPE_IMAGE_CONFIG)
168            .await
169    }
170
171    /// Retrieve the blob from the registry identified by `digest`.
172    pub async fn blob(&self, image: &str, digest: &Digest) -> Result<Blob, ErrorResponse> {
173        let url = format!("{}/v2/{}/blobs/{}", &self.api_url, image, digest);
174        let mut request = self.client.get(&url);
175        if let Some(token) = self.auth_token.clone() {
176            request = request.bearer_auth(token.access_token);
177        }
178
179        let response = request.send().await?;
180
181        match response.status() {
182            StatusCode::OK => Ok(Blob::from(response)),
183            _ => Err(ErrorResponse::APIError(response.json::<ErrorList>().await?)),
184        }
185    }
186
187    async fn request<T: serde::de::DeserializeOwned>(
188        &self,
189        method: Method,
190        url: &str,
191        accept: &str,
192    ) -> Result<T, ErrorResponse> {
193        let mut request = self
194            .client
195            .request(method, url)
196            .header(reqwest::header::ACCEPT, accept);
197
198        if let Some(token) = self.auth_token.clone() {
199            request = request.bearer_auth(token.access_token);
200        }
201
202        let response = request.send().await?;
203
204        match response.status() {
205            StatusCode::OK => Ok(response.json::<T>().await?),
206            _ => Err(ErrorResponse::APIError(response.json::<ErrorList>().await?)),
207        }
208    }
209}
210
211/// OAuth 2.0 token.
212#[allow(dead_code)]
213#[derive(serde::Deserialize, Clone, Debug)]
214pub struct AuthToken {
215    access_token: String,
216    expires_in: i32,
217    issued_at: String,
218}