1mod registry;
34
35use std::collections::HashMap;
36
37use anyhow::{anyhow, Context, Result};
38use reqwest::{header::ACCEPT, Certificate, Client};
39use thiserror::Error;
40
41use registry::*;
42
43#[derive(Error, Debug)]
44pub enum AduanaError {
45 #[error("Cannot connect to {url}: {reason}")]
46 Connection { url: String, reason: String },
47 #[error(transparent)]
48 Runtime(#[from] anyhow::Error),
49}
50
51impl From<reqwest::Error> for AduanaError {
52 fn from(error: reqwest::Error) -> Self {
53 if error.is_connect() || error.is_builder() {
54 let url = error
55 .url()
56 .map(|url| url.to_string())
57 .unwrap_or_else(|| "invalid".to_string());
58 AduanaError::Connection {
59 url,
60 reason: error.to_string(),
61 }
62 } else {
63 AduanaError::Runtime(anyhow!(
64 "Failed to get images from {:?}. {:?}",
65 error.url(),
66 error
67 ))
68 }
69 }
70}
71
72#[derive(Debug, Clone)]
73pub struct AduanaImage<'a> {
74 inspector: &'a AduanaInspector,
75 name: String,
76 tags: Vec<String>,
77}
78
79#[derive(Debug, Clone)]
80pub struct ImageDetails {
81 pub name: String,
82 pub tag: String,
83 pub user: Option<String>,
84 pub env: Vec<String>,
85 pub cmd: Vec<String>,
86 pub working_dir: Option<String>,
87 pub labels: HashMap<String, String>,
88 pub arch: String,
89 pub created: String,
90}
91
92fn client(pem: &Option<Vec<u8>>) -> Result<Client, AduanaError> {
93 let mut builder = reqwest::Client::builder();
94
95 if let Some(bytes) = pem {
96 let cert = Certificate::from_pem(bytes)
97 .with_context(|| "Failed to parse PEM certificate".to_string())?;
98 builder = builder.add_root_certificate(cert);
99 }
100
101 let client = builder
102 .build()
103 .with_context(|| "Failed to build reqwest client!")?;
104
105 Ok(client)
106}
107
108impl<'a> AduanaImage<'a> {
109 pub fn name(&self) -> &str {
111 &self.name
112 }
113
114 pub fn tags(&self) -> &[String] {
116 &self.tags
117 }
118
119 pub async fn details(&self, tag: &str) -> Result<ImageDetails, AduanaError> {
121 let url = format!(
122 "{}/v2/{}/manifests/{}",
123 &self.inspector.url, &self.name, tag
124 );
125 let client = client(&self.inspector.cert)?;
126 let response = client
127 .get(&url)
128 .header(
129 ACCEPT,
130 "application/vnd.docker.distribution.manifest.v2+json",
131 )
132 .send()
133 .await?;
134 let manifest: ResponseManifest = response.json().await?;
135 let blob = self.retrieve_blob(&manifest.config.digest).await?;
136
137 let result = ImageDetails {
138 name: self.name.clone(),
139 tag: tag.to_string(),
140 user: blob.config.user,
141 env: blob.config.env,
142 cmd: blob.config.cmd,
143 working_dir: blob.config.working_dir,
144 labels: blob.config.labels,
145 arch: blob.architecture,
146 created: blob.created,
147 };
148
149 Ok(result)
150 }
151
152 async fn retrieve_blob(&self, digest: &str) -> Result<ResponseConfigBlob, AduanaError> {
153 let url = format!("{}/v2/{}/blobs/{}", &self.inspector.url, &self.name, digest);
154 let client = client(&self.inspector.cert)?;
155 let response = client.get(&url).send().await?;
156 let details: ResponseConfigBlob = response.json().await?;
157 Ok(details)
158 }
159}
160
161#[derive(Clone)]
162pub struct AduanaInspector {
163 url: String,
164 cert: Option<Vec<u8>>,
165}
166
167impl std::fmt::Debug for AduanaInspector {
168 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
169 write!(
170 f,
171 "AduanaInspector {{ url: {}, cert: {} }}",
172 &self.url,
173 self.cert.is_some()
174 )
175 }
176}
177
178impl AduanaInspector {
179 pub fn new(url: &str) -> Self {
180 AduanaInspector {
181 url: url.to_string(),
182 cert: None,
183 }
184 }
185
186 pub fn with_cert(mut self, pem: Vec<u8>) -> Self {
187 self.cert = Some(pem);
188 self
189 }
190
191 pub fn url(&self) -> &str {
192 &self.url
193 }
194
195 pub async fn images(&'_ self) -> Result<Vec<AduanaImage<'_>>, AduanaError> {
196 let url = format!("{}/v2/_catalog", &self.url);
197 let client = client(&self.cert)?;
198 let response = client.get(&url).send().await?;
199
200 let mut images = Vec::new();
201 let catalog: ResponseCatalog = response
202 .json()
203 .await
204 .with_context(|| "Failed to parse catalog response")?;
205 for name in catalog.repositories {
206 let image = self.retrieve_image(&name).await?;
207 let image = AduanaImage {
208 inspector: self,
209 name: image.name,
210 tags: image.tags,
211 };
212 images.push(image);
213 }
214 Ok(images)
215 }
216
217 async fn retrieve_image(&self, name: &str) -> Result<ResponseImage, AduanaError> {
218 let url = format!("{}/v2/{}/tags/list", &self.url, name);
219 let client = client(&self.cert)?;
220 let response = client.get(&url).send().await?;
221 let image: ResponseImage = response.json().await?;
222 Ok(image)
223 }
224}
225
226#[cfg(test)]
227mod tests {
228
229 use std::fs::File;
230 use std::io::Read;
231
232 use super::*;
233
234 #[tokio::test]
235 #[ignore="requires a running local registry"]
236 async fn test_images() {
237 let mut pem = Vec::new();
238 let mut file = File::open("certs/ca.crt").unwrap();
239 file.read_to_end(&mut pem).unwrap();
240
241 let inspector = AduanaInspector::new("https://localhost:5000").with_cert(pem);
242 let images = inspector.images().await.unwrap();
243 println!("{:#?}", images);
244 }
245
246 #[tokio::test]
247 #[ignore="requires a running local registry"]
248 async fn test_details() {
249 let mut pem = Vec::new();
250 let mut file = File::open("certs/ca.crt").unwrap();
251 file.read_to_end(&mut pem).unwrap();
252
253 let inspector = AduanaInspector::new("https://localhost:5000").with_cert(pem);
254 let images = inspector.images().await.unwrap();
255
256 for image in images {
257 for tag in image.tags() {
258 let details = image.details(tag).await.unwrap();
259 println!("{:#?}", details);
260 }
261 }
262 }
263
264 #[tokio::test]
265 async fn wrong_url() {
266 let inspector = AduanaInspector::new(":xx:x");
267 match inspector.images().await {
268 Err(AduanaError::Connection { url, reason: _ }) => {
269 assert_eq!(&url, "invalid");
270 }
271 Err(other) => panic!("Unexpected error! {:#?}", other),
272 Ok(result) => panic!("Should not get result back! {:#?}", result),
273 }
274 }
275}