1use log::trace;
2use reqwest::Certificate;
3
4use crate::{mediatypes::MediaTypes, v2::*};
5
6#[derive(Clone, Debug)]
8pub struct Config {
9 index: String,
10 insecure_registry: bool,
11 user_agent: Option<String>,
12 username: Option<String>,
13 password: Option<String>,
14 accept_invalid_certs: bool,
15 root_certificates: Vec<Certificate>,
16 accepted_types: Option<Vec<(MediaTypes, Option<f64>)>>,
17 connect_timeout: Option<std::time::Duration>,
18 request_timeout: Option<std::time::Duration>,
19}
20
21impl Config {
22 #[must_use]
24 pub fn registry(mut self, reg: &str) -> Self {
25 self.index = reg.to_owned();
26 self
27 }
28
29 #[must_use]
31 pub fn insecure_registry(mut self, insecure: bool) -> Self {
32 self.insecure_registry = insecure;
33 self
34 }
35
36 #[must_use]
38 pub fn accept_invalid_certs(mut self, accept_invalid_certs: bool) -> Self {
39 self.accept_invalid_certs = accept_invalid_certs;
40 self
41 }
42
43 #[must_use]
45 pub fn add_root_certificate(mut self, certificate: Certificate) -> Self {
46 self.root_certificates.push(certificate);
47 self
48 }
49
50 #[must_use]
52 pub fn accepted_types(mut self, accepted_types: Option<Vec<(MediaTypes, Option<f64>)>>) -> Self {
53 self.accepted_types = accepted_types;
54 self
55 }
56
57 #[must_use]
59 pub fn user_agent(mut self, user_agent: Option<String>) -> Self {
60 self.user_agent = user_agent;
61 self
62 }
63
64 #[must_use]
66 pub fn username(mut self, user: Option<String>) -> Self {
67 self.username = user;
68 self
69 }
70
71 #[must_use]
73 pub fn password(mut self, password: Option<String>) -> Self {
74 self.password = password;
75 self
76 }
77
78 #[must_use]
80 pub fn read_credentials<T: ::std::io::Read>(mut self, reader: T) -> Self {
81 if let Ok(creds) = crate::get_credentials(reader, &self.index) {
82 self.username = creds.0;
83 self.password = creds.1;
84 };
85 self
86 }
87
88 #[must_use]
90 pub fn connect_timeout(mut self, timeout: std::time::Duration) -> Self {
91 self.connect_timeout = Some(timeout);
92 self
93 }
94
95 #[must_use]
97 pub fn request_timeout(mut self, timeout: std::time::Duration) -> Self {
98 self.request_timeout = Some(timeout);
99 self
100 }
101
102 pub fn build(self) -> Result<Client> {
104 let base = if self.insecure_registry {
105 "http://".to_string() + &self.index
106 } else {
107 "https://".to_string() + &self.index
108 };
109 trace!("Built client for {:?}: endpoint {:?}", self.index, base);
110 let creds = match (self.username, self.password) {
111 (None, None) => None,
112 (u, p) => Some((u.unwrap_or_else(|| "".into()), p.unwrap_or_else(|| "".into()))),
113 };
114
115 if self.insecure_registry && creds.is_some() {
116 log::warn!(
117 "Registry {:?} is configured with insecure HTTP; credentials will be transmitted in cleartext",
118 self.index
119 );
120 }
121
122 let mut builder = reqwest::ClientBuilder::new().danger_accept_invalid_certs(self.accept_invalid_certs);
123
124 if let Some(timeout) = self.connect_timeout {
125 builder = builder.connect_timeout(timeout);
126 }
127 if let Some(timeout) = self.request_timeout {
128 builder = builder.timeout(timeout);
129 }
130
131 for ca in self.root_certificates {
132 builder = builder.add_root_certificate(ca)
133 }
134
135 let client = builder.build()?;
136
137 let accepted_types = match self.accepted_types {
138 Some(a) => a,
139 None => match self.index == "gcr.io" || self.index.ends_with(".gcr.io") || self.index.ends_with(".k8s.io") {
140 false => vec![
141 (MediaTypes::ManifestV2S2, Some(0.5)),
144 (MediaTypes::ManifestV2S1Signed, Some(0.4)),
145 (MediaTypes::ManifestList, Some(0.5)),
146 (MediaTypes::OciImageManifest, Some(0.5)),
147 (MediaTypes::OciImageIndexV1, Some(0.5)),
148 ],
149 true => vec![
154 (MediaTypes::ManifestV2S2, None),
155 (MediaTypes::ManifestV2S1Signed, None),
156 (MediaTypes::ManifestList, None),
157 (MediaTypes::OciImageManifest, None),
158 (MediaTypes::OciImageIndexV1, None),
159 ],
160 },
161 };
162 let c = Client {
163 base_url: base,
164 credentials: creds,
165 user_agent: self.user_agent,
166 auth: None,
167 client,
168 accepted_types,
169 };
170 Ok(c)
171 }
172}
173
174impl Default for Config {
175 fn default() -> Self {
177 Self {
178 index: "registry-1.docker.io".into(),
179 insecure_registry: false,
180 accept_invalid_certs: false,
181 root_certificates: Default::default(),
182 accepted_types: None,
183 user_agent: Some(crate::USER_AGENT.to_owned()),
184 username: None,
185 password: None,
186 connect_timeout: None,
187 request_timeout: None,
188 }
189 }
190}
191
192#[cfg(test)]
193mod tests {
194 use super::*;
195
196 #[test]
197 fn test_config_default() {
198 let config = Config::default();
199 assert_eq!(config.index, "registry-1.docker.io");
200 assert!(!config.insecure_registry);
201 assert!(!config.accept_invalid_certs);
202 assert!(config.username.is_none());
203 assert!(config.password.is_none());
204 assert!(config.accepted_types.is_none());
205 assert_eq!(config.user_agent.as_deref(), Some(crate::USER_AGENT));
206 }
207
208 #[test]
209 fn test_config_builder_chaining() {
210 let config = Config::default()
211 .registry("myregistry.io")
212 .insecure_registry(true)
213 .accept_invalid_certs(true)
214 .username(Some("user".to_string()))
215 .password(Some("pass".to_string()))
216 .user_agent(Some("test-agent".to_string()));
217
218 assert_eq!(config.index, "myregistry.io");
219 assert!(config.insecure_registry);
220 assert!(config.accept_invalid_certs);
221 assert_eq!(config.username.as_deref(), Some("user"));
222 assert_eq!(config.password.as_deref(), Some("pass"));
223 assert_eq!(config.user_agent.as_deref(), Some("test-agent"));
224 }
225
226 #[test]
227 fn test_config_build_insecure() {
228 let client = Config::default()
229 .registry("localhost:5000")
230 .insecure_registry(true)
231 .build()
232 .unwrap();
233 assert!(client.base_url.starts_with("http://"));
234 }
235
236 #[test]
237 fn test_config_build_secure() {
238 let client = Config::default()
239 .registry("myregistry.io")
240 .insecure_registry(false)
241 .build()
242 .unwrap();
243 assert!(client.base_url.starts_with("https://"));
244 }
245
246 #[test]
247 fn test_config_build_credentials() {
248 let client = Config::default()
249 .registry("myregistry.io")
250 .username(Some("user".to_string()))
251 .password(Some("pass".to_string()))
252 .build()
253 .unwrap();
254 assert_eq!(client.credentials, Some(("user".to_string(), "pass".to_string())));
255 }
256
257 #[test]
258 fn test_config_build_no_credentials() {
259 let client = Config::default().registry("myregistry.io").build().unwrap();
260 assert!(client.credentials.is_none());
261 }
262
263 #[test]
264 fn test_config_build_partial_credentials_username_only() {
265 let client = Config::default()
266 .registry("myregistry.io")
267 .username(Some("user".to_string()))
268 .build()
269 .unwrap();
270 assert_eq!(client.credentials, Some(("user".to_string(), "".to_string())));
271 }
272
273 #[test]
274 fn test_config_read_credentials() {
275 let config_json = r#"{"auths":{"myregistry.io":{"auth":"dXNlcjpwYXNz"}}}"#;
277 let config = Config::default()
278 .registry("myregistry.io")
279 .read_credentials(config_json.as_bytes());
280 assert_eq!(config.username.as_deref(), Some("user"));
281 assert_eq!(config.password.as_deref(), Some("pass"));
282 }
283
284 #[test]
285 fn test_config_read_credentials_missing_registry() {
286 let config_json = r#"{"auths":{"other.io":{"auth":"dXNlcjpwYXNz"}}}"#;
287 let config = Config::default()
288 .registry("myregistry.io")
289 .read_credentials(config_json.as_bytes());
290 assert!(config.username.is_none());
292 assert!(config.password.is_none());
293 }
294
295 #[test]
296 fn test_config_timeout_fields() {
297 let config = Config::default()
298 .connect_timeout(std::time::Duration::from_secs(5))
299 .request_timeout(std::time::Duration::from_secs(30));
300
301 assert_eq!(config.connect_timeout, Some(std::time::Duration::from_secs(5)));
302 assert_eq!(config.request_timeout, Some(std::time::Duration::from_secs(30)));
303 }
304}