gr/cmds/
docker.rs

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    // If set, list all remote repositories in project's registry
17    pub repos: bool,
18    // If set, list all tags for a repository
19    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}