1use std::{io::Write, sync::Arc};
2
3use crate::{
4 api_traits::{ContainerRegistry, Timestamp},
5 cli::docker::DockerOptions,
6 config::ConfigProperties,
7 display::{self, Column, DisplayBody},
8 remote::{self, get_registry, CacheType, GetRemoteCliArgs, ListBodyArgs, ListRemoteCliArgs},
9 Result,
10};
11
12use super::common::{process_num_metadata, MetadataName};
13
14#[derive(Builder)]
15pub struct DockerListCliArgs {
16 pub repos: bool,
18 pub tags: bool,
20 pub repo_id: Option<i64>,
21 pub list_args: ListRemoteCliArgs,
22}
23
24impl DockerListCliArgs {
25 pub fn builder() -> DockerListCliArgsBuilder {
26 DockerListCliArgsBuilder::default()
27 }
28}
29
30#[derive(Builder)]
31pub struct DockerListBodyArgs {
32 #[builder(default)]
33 pub repos: bool,
34 #[builder(default)]
35 pub tags: bool,
36 #[builder(default)]
37 pub repo_id: Option<i64>,
38 #[builder(default)]
39 pub body_args: Option<ListBodyArgs>,
40}
41
42impl DockerListBodyArgs {
43 pub fn builder() -> DockerListBodyArgsBuilder {
44 DockerListBodyArgsBuilder::default()
45 }
46}
47
48#[derive(Builder, Clone)]
49pub struct RegistryRepository {
50 pub id: i64,
51 pub location: String,
52 pub tags_count: i64,
53 pub created_at: String,
54}
55
56impl RegistryRepository {
57 pub fn builder() -> RegistryRepositoryBuilder {
58 RegistryRepositoryBuilder::default()
59 }
60}
61
62impl From<RegistryRepository> for DisplayBody {
63 fn from(repo: RegistryRepository) -> DisplayBody {
64 DisplayBody::new(vec![
65 Column::new("ID", repo.id.to_string()),
66 Column::new("Location", repo.location),
67 Column::new("Tags count", repo.tags_count.to_string()),
68 Column::new("Created at", repo.created_at),
69 ])
70 }
71}
72
73impl Timestamp for RegistryRepository {
74 fn created_at(&self) -> String {
75 self.created_at.clone()
76 }
77}
78
79#[derive(Builder, Clone)]
80pub struct RepositoryTag {
81 pub name: String,
82 pub path: String,
83 pub location: String,
84 pub created_at: String,
85}
86
87impl RepositoryTag {
88 pub fn builder() -> RepositoryTagBuilder {
89 RepositoryTagBuilder::default()
90 }
91}
92
93impl Timestamp for RepositoryTag {
94 fn created_at(&self) -> String {
95 self.created_at.clone()
96 }
97}
98
99impl From<RepositoryTag> for DisplayBody {
100 fn from(tag: RepositoryTag) -> DisplayBody {
101 DisplayBody::new(vec![
102 Column::new("Name", tag.name),
103 Column::new("Path", tag.path),
104 Column::new("Location", tag.location),
105 ])
106 }
107}
108
109#[derive(Builder)]
110pub struct DockerImageCliArgs {
111 pub tag: String,
112 pub repo_id: i64,
113 pub get_args: GetRemoteCliArgs,
114}
115
116impl DockerImageCliArgs {
117 pub fn builder() -> DockerImageCliArgsBuilder {
118 DockerImageCliArgsBuilder::default()
119 }
120}
121
122#[derive(Builder, Clone)]
123pub struct ImageMetadata {
124 pub name: String,
125 pub location: String,
126 pub short_sha: String,
127 pub size: i64,
128 pub created_at: String,
129}
130
131impl ImageMetadata {
132 pub fn builder() -> ImageMetadataBuilder {
133 ImageMetadataBuilder::default()
134 }
135}
136
137impl From<ImageMetadata> for DisplayBody {
138 fn from(metadata: ImageMetadata) -> DisplayBody {
139 DisplayBody::new(vec![
140 Column::new("Name", metadata.name),
141 Column::new("Location", metadata.location),
142 Column::new("Short SHA", metadata.short_sha),
143 Column::new("Size", metadata.size.to_string()),
144 Column::new("Created at", metadata.created_at),
145 ])
146 }
147}
148
149pub fn execute(
150 options: DockerOptions,
151 config: Arc<dyn ConfigProperties>,
152 domain: String,
153 path: String,
154) -> Result<()> {
155 match options {
156 DockerOptions::List(cli_args) => {
157 let remote = get_registry(
158 domain,
159 path,
160 config,
161 Some(&cli_args.list_args.get_args.cache_args),
162 CacheType::File,
163 )?;
164 validate_and_list(remote, cli_args, std::io::stdout())
165 }
166 DockerOptions::Get(cli_args) => {
167 let remote = get_registry(
168 domain,
169 path,
170 config,
171 Some(&cli_args.get_args.cache_args),
172 CacheType::File,
173 )?;
174 get_image_metadata(remote, cli_args, std::io::stdout())
175 }
176 }
177}
178
179fn get_image_metadata<W: Write>(
180 remote: Arc<dyn ContainerRegistry + Send + Sync>,
181 cli_args: DockerImageCliArgs,
182 mut writer: W,
183) -> Result<()> {
184 let metadata = remote.get_image_metadata(cli_args.repo_id, &cli_args.tag)?;
185 display::print(&mut writer, vec![metadata], cli_args.get_args)?;
186 Ok(())
187}
188
189fn validate_and_list<W: Write>(
190 remote: Arc<dyn ContainerRegistry + Send + Sync>,
191 cli_args: DockerListCliArgs,
192 mut writer: W,
193) -> Result<()> {
194 if cli_args.list_args.num_pages {
195 return get_num_pages(remote, cli_args, writer);
196 }
197 if cli_args.list_args.num_resources {
198 return get_num_resources(remote, cli_args, writer);
199 }
200 let body_args = remote::validate_from_to_page(&cli_args.list_args)?;
201 let body_args = DockerListBodyArgs::builder()
202 .repos(cli_args.repos)
203 .tags(cli_args.tags)
204 .repo_id(cli_args.repo_id)
205 .body_args(body_args)
206 .build()?;
207 if body_args.tags {
208 let tags = remote.list_repository_tags(body_args)?;
209 display::print(&mut writer, tags, cli_args.list_args.get_args)?;
210 return Ok(());
211 }
212 let repos = remote.list_repositories(body_args)?;
213 display::print(&mut writer, repos, cli_args.list_args.get_args)
214}
215
216fn get_num_pages<W: Write>(
217 remote: Arc<dyn ContainerRegistry + Send + Sync>,
218 cli_args: DockerListCliArgs,
219 writer: W,
220) -> Result<()> {
221 if cli_args.tags {
222 let result = remote.num_pages_repository_tags(cli_args.repo_id.unwrap());
223 return process_num_metadata(result, MetadataName::Pages, writer);
224 }
225 let result = remote.num_pages_repositories();
226 process_num_metadata(result, MetadataName::Pages, writer)
227}
228
229fn get_num_resources<W: Write>(
230 remote: Arc<dyn ContainerRegistry + Send + Sync>,
231 cli_args: DockerListCliArgs,
232 writer: W,
233) -> Result<()> {
234 if cli_args.tags {
235 let result = remote.num_resources_repository_tags(cli_args.repo_id.unwrap());
236 return process_num_metadata(result, MetadataName::Resources, writer);
237 }
238 let result = remote.num_resources_repositories();
239 process_num_metadata(result, MetadataName::Resources, writer)
240}
241
242#[cfg(test)]
243mod tests {
244 use remote::CacheCliArgs;
245
246 use crate::error;
247
248 use super::*;
249
250 #[derive(Builder, Default)]
251 struct MockContainerRegistry {
252 #[builder(default)]
253 num_pages_repos_ok_none: bool,
254 #[builder(default)]
255 num_pages_repos_err: bool,
256 }
257
258 impl MockContainerRegistry {
259 pub fn new() -> MockContainerRegistry {
260 MockContainerRegistry::default()
261 }
262 pub fn builder() -> MockContainerRegistryBuilder {
263 MockContainerRegistryBuilder::default()
264 }
265 }
266
267 impl ContainerRegistry for MockContainerRegistry {
268 fn list_repositories(&self, _args: DockerListBodyArgs) -> Result<Vec<RegistryRepository>> {
269 let repo = RegistryRepository::builder()
270 .id(1)
271 .location("registry.gitlab.com/namespace/project".to_string())
272 .tags_count(10)
273 .created_at("2021-01-01T00:00:00Z".to_string())
274 .build()
275 .unwrap();
276 Ok(vec![repo])
277 }
278
279 fn list_repository_tags(&self, _args: DockerListBodyArgs) -> Result<Vec<RepositoryTag>> {
280 let tag = RepositoryTag::builder()
281 .name("v0.0.1".to_string())
282 .path("namespace/project:v0.0.1".to_string())
283 .location("registry.gitlab.com/namespace/project:v0.0.1".to_string())
284 .created_at("2021-01-01T00:00:00Z".to_string())
285 .build()
286 .unwrap();
287 Ok(vec![tag])
288 }
289
290 fn num_pages_repository_tags(&self, _repository_id: i64) -> Result<Option<u32>> {
291 Ok(Some(3))
292 }
293
294 fn num_pages_repositories(&self) -> Result<Option<u32>> {
295 if self.num_pages_repos_ok_none {
296 return Ok(None);
297 }
298 if self.num_pages_repos_err {
299 return Err(error::gen("Error"));
300 }
301 Ok(Some(1))
302 }
303
304 fn get_image_metadata(&self, _repository_id: i64, tag: &str) -> Result<ImageMetadata> {
305 let metadata = ImageMetadata::builder()
306 .name(tag.to_string())
307 .location(format!("registry.gitlab.com/namespace/project:{tag}"))
308 .short_sha("12345678".to_string())
309 .size(100)
310 .created_at("2021-01-01T00:00:00Z".to_string())
311 .build()
312 .unwrap();
313 Ok(metadata)
314 }
315
316 fn num_resources_repository_tags(
317 &self,
318 _repository_id: i64,
319 ) -> Result<Option<crate::api_traits::NumberDeltaErr>> {
320 todo!()
321 }
322
323 fn num_resources_repositories(&self) -> Result<Option<crate::api_traits::NumberDeltaErr>> {
324 todo!()
325 }
326 }
327
328 #[test]
329 fn test_execute_list_repositories() {
330 let remote = Arc::new(MockContainerRegistry::new());
331 let args = DockerListCliArgs::builder()
332 .repos(true)
333 .tags(false)
334 .repo_id(None)
335 .list_args(
336 ListRemoteCliArgs::builder()
337 .get_args(
338 GetRemoteCliArgs::builder()
339 .cache_args(CacheCliArgs::default())
340 .build()
341 .unwrap(),
342 )
343 .build()
344 .unwrap(),
345 )
346 .build()
347 .unwrap();
348 let mut buf = Vec::new();
349 validate_and_list(remote, args, &mut buf).unwrap();
350 assert_eq!(
351 "ID|Location|Tags count|Created at\n\
352 1|registry.gitlab.com/namespace/project|10|2021-01-01T00:00:00Z\n",
353 String::from_utf8(buf).unwrap()
354 );
355 }
356
357 #[test]
358 fn test_execute_list_tags() {
359 let remote = Arc::new(MockContainerRegistry::new());
360 let args = DockerListCliArgs::builder()
361 .repos(false)
362 .tags(true)
363 .repo_id(Some(1))
364 .list_args(
365 ListRemoteCliArgs::builder()
366 .get_args(
367 GetRemoteCliArgs::builder()
368 .cache_args(CacheCliArgs::default())
369 .build()
370 .unwrap(),
371 )
372 .build()
373 .unwrap(),
374 )
375 .build()
376 .unwrap();
377 let mut buf = Vec::new();
378 validate_and_list(remote, args, &mut buf).unwrap();
379 assert_eq!(
380 "Name|Path|Location\n\
381 v0.0.1|namespace/project:v0.0.1|registry.gitlab.com/namespace/project:v0.0.1\n",
382 String::from_utf8(buf).unwrap()
383 );
384 }
385
386 #[test]
387 fn test_get_num_pages_for_listing_tags() {
388 let remote = Arc::new(MockContainerRegistry::new());
389 let args = DockerListCliArgs::builder()
390 .repos(false)
391 .tags(true)
392 .repo_id(Some(1))
393 .list_args(
394 ListRemoteCliArgs::builder()
395 .get_args(
396 GetRemoteCliArgs::builder()
397 .cache_args(CacheCliArgs::default())
398 .build()
399 .unwrap(),
400 )
401 .num_pages(true)
402 .build()
403 .unwrap(),
404 )
405 .build()
406 .unwrap();
407 let mut buf = Vec::new();
408 validate_and_list(remote, args, &mut buf).unwrap();
409 assert_eq!("3\n", String::from_utf8(buf).unwrap());
410 }
411
412 #[test]
413 fn test_get_num_pages_for_listing_repositories() {
414 let remote = Arc::new(MockContainerRegistry::new());
415 let args = DockerListCliArgs::builder()
416 .repos(true)
417 .tags(false)
418 .repo_id(None)
419 .list_args(
420 ListRemoteCliArgs::builder()
421 .get_args(
422 GetRemoteCliArgs::builder()
423 .cache_args(CacheCliArgs::default())
424 .build()
425 .unwrap(),
426 )
427 .num_pages(true)
428 .build()
429 .unwrap(),
430 )
431 .build()
432 .unwrap();
433 let mut buf = Vec::new();
434 validate_and_list(remote, args, &mut buf).unwrap();
435 assert_eq!("1\n", String::from_utf8(buf).unwrap());
436 }
437
438 #[test]
439 fn test_do_not_print_headers_if_no_headers_provided_for_tags() {
440 let remote = Arc::new(MockContainerRegistry::new());
441 let args = DockerListCliArgs::builder()
442 .repos(false)
443 .tags(true)
444 .repo_id(Some(1))
445 .list_args(
446 ListRemoteCliArgs::builder()
447 .get_args(
448 GetRemoteCliArgs::builder()
449 .cache_args(CacheCliArgs::default())
450 .no_headers(true)
451 .build()
452 .unwrap(),
453 )
454 .build()
455 .unwrap(),
456 )
457 .build()
458 .unwrap();
459 let mut buf = Vec::new();
460 validate_and_list(remote, args, &mut buf).unwrap();
461 assert_eq!(
462 "v0.0.1|namespace/project:v0.0.1|registry.gitlab.com/namespace/project:v0.0.1\n",
463 String::from_utf8(buf).unwrap()
464 );
465 }
466
467 #[test]
468 fn test_do_not_print_headers_if_no_headers_provided_for_repositories() {
469 let remote = Arc::new(MockContainerRegistry::new());
470 let args = DockerListCliArgs::builder()
471 .repos(true)
472 .tags(false)
473 .repo_id(None)
474 .list_args(
475 ListRemoteCliArgs::builder()
476 .get_args(
477 GetRemoteCliArgs::builder()
478 .cache_args(CacheCliArgs::default())
479 .no_headers(true)
480 .build()
481 .unwrap(),
482 )
483 .build()
484 .unwrap(),
485 )
486 .build()
487 .unwrap();
488 let mut buf = Vec::new();
489 validate_and_list(remote, args, &mut buf).unwrap();
490 assert_eq!(
491 "1|registry.gitlab.com/namespace/project|10|2021-01-01T00:00:00Z\n",
492 String::from_utf8(buf).unwrap()
493 );
494 }
495
496 #[test]
497 fn test_num_pages_not_available_in_headers() {
498 let remote = Arc::new(
499 MockContainerRegistry::builder()
500 .num_pages_repos_ok_none(true)
501 .build()
502 .unwrap(),
503 );
504 let args = DockerListCliArgs::builder()
505 .repos(true)
506 .tags(false)
507 .repo_id(None)
508 .list_args(
509 ListRemoteCliArgs::builder()
510 .get_args(
511 GetRemoteCliArgs::builder()
512 .cache_args(CacheCliArgs::default())
513 .no_headers(true)
514 .build()
515 .unwrap(),
516 )
517 .num_pages(true)
518 .build()
519 .unwrap(),
520 )
521 .build()
522 .unwrap();
523 let mut buf = Vec::new();
524 validate_and_list(remote, args, &mut buf).unwrap();
525 assert_eq!(
526 "Number of pages not available.\n",
527 String::from_utf8(buf).unwrap()
528 );
529 }
530
531 #[test]
532 fn test_num_pages_error_in_remote_is_error() {
533 let remote = Arc::new(
534 MockContainerRegistry::builder()
535 .num_pages_repos_err(true)
536 .build()
537 .unwrap(),
538 );
539 let args = DockerListCliArgs::builder()
540 .repos(true)
541 .tags(false)
542 .repo_id(None)
543 .list_args(
544 ListRemoteCliArgs::builder()
545 .get_args(
546 GetRemoteCliArgs::builder()
547 .cache_args(CacheCliArgs::default())
548 .no_headers(true)
549 .build()
550 .unwrap(),
551 )
552 .num_pages(true)
553 .build()
554 .unwrap(),
555 )
556 .build()
557 .unwrap();
558 let mut buf = Vec::new();
559 assert!(validate_and_list(remote, args, &mut buf).is_err());
560 }
561
562 #[test]
563 fn test_get_image_metadata() {
564 let remote = Arc::new(MockContainerRegistry::new());
565 let args = DockerImageCliArgs::builder()
566 .tag("v0.0.1".to_string())
567 .repo_id(1)
568 .get_args(GetRemoteCliArgs::builder().build().unwrap())
569 .build()
570 .unwrap();
571 let mut buf = Vec::new();
572 get_image_metadata(remote, args, &mut buf).unwrap();
573 assert_eq!(
574 "Name|Location|Short SHA|Size|Created at\n\
575 v0.0.1|registry.gitlab.com/namespace/project:v0.0.1|12345678|100|2021-01-01T00:00:00Z\n",
576 String::from_utf8(buf).unwrap()
577 );
578 }
579
580 #[test]
581 fn test_get_image_metadata_no_headers() {
582 let remote = Arc::new(MockContainerRegistry::new());
583 let args = DockerImageCliArgs::builder()
584 .tag("v0.0.1".to_string())
585 .repo_id(1)
586 .get_args(
587 GetRemoteCliArgs::builder()
588 .cache_args(CacheCliArgs::default())
589 .no_headers(true)
590 .build()
591 .unwrap(),
592 )
593 .build()
594 .unwrap();
595 let mut buf = Vec::new();
596 get_image_metadata(remote, args, &mut buf).unwrap();
597 assert_eq!(
598 "v0.0.1|registry.gitlab.com/namespace/project:v0.0.1|12345678|100|2021-01-01T00:00:00Z\n",
599 String::from_utf8(buf).unwrap()
600 );
601 }
602}