1pub 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#[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 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 pub fn set_auth_token(&mut self, token: Option<AuthToken>) {
108 self.auth_token = token;
109 }
110
111 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 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 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 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 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 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#[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}