1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
use testcontainers::{core::WaitFor, Image};

const NAME: &str = "registry";
const TAG: &str = "2";

/// Module to work with a custom Docker registry inside of tests.
///
/// Starts an instance of [`CNCF Distribution`], an easy-to-use registry for container images.
///
/// # Example
/// ```
/// use testcontainers_modules::{cncf_distribution, testcontainers::runners::SyncRunner};
///
/// let registry = cncf_distribution::CncfDistribution.start().unwrap();
///
/// let image_name = "test";
/// let image_tag = format!(
///     "{}:{}/{image_name}",
///     registry.get_host().unwrap(),
///     registry.get_host_port_ipv4(5000).unwrap()
/// );
///
/// // now you can push an image tagged with `image_tag` and pull it afterward
/// ```
///
/// [`CNCF Distribution`]: https://distribution.github.io/distribution/
#[derive(Debug, Default, Clone)]
pub struct CncfDistribution;

impl Image for CncfDistribution {
    fn name(&self) -> &str {
        NAME
    }

    fn tag(&self) -> &str {
        TAG
    }

    fn ready_conditions(&self) -> Vec<WaitFor> {
        vec![WaitFor::message_on_stderr("listening on [::]:5000")]
    }
}

#[cfg(test)]
mod tests {
    use bollard::image::{BuildImageOptions, CreateImageOptions};
    use futures::StreamExt;

    use crate::{cncf_distribution, testcontainers::runners::AsyncRunner};

    const DOCKERFILE: &[u8] = b"
        FROM scratch
        COPY Dockerfile /
    ";

    #[tokio::test]
    async fn distribution_push_pull_image() -> Result<(), Box<dyn std::error::Error + 'static>> {
        let _ = pretty_env_logger::try_init();
        let distribution_node = cncf_distribution::CncfDistribution.start().await?;
        let docker = bollard::Docker::connect_with_local_defaults().unwrap();
        let image_tag = format!(
            "localhost:{}/test:latest",
            distribution_node.get_host_port_ipv4(5000).await?
        );

        let mut archive = tar::Builder::new(Vec::new());
        let mut header = tar::Header::new_gnu();
        header.set_path("Dockerfile").unwrap();
        header.set_size(DOCKERFILE.len() as u64);
        header.set_cksum();
        archive.append(&header, DOCKERFILE).unwrap();

        // Build test image
        let mut build_image = docker.build_image(
            BuildImageOptions {
                dockerfile: "Dockerfile",
                t: &image_tag,
                ..Default::default()
            },
            None,
            Some(archive.into_inner().unwrap().into()),
        );
        while let Some(x) = build_image.next().await {
            println!("Build status: {:?}", x.unwrap());
        }

        // Push image, and then remove it
        let mut push_image = docker.push_image::<String>(&image_tag, None, None);
        while let Some(x) = push_image.next().await {
            println!("Push image: {:?}", x.unwrap());
        }
        docker.remove_image(&image_tag, None, None).await.unwrap();

        // Pull image
        let mut create_image = docker.create_image(
            Some(CreateImageOptions {
                from_image: image_tag.as_str(),
                ..Default::default()
            }),
            None,
            None,
        );
        while let Some(x) = create_image.next().await {
            println!("Create image: {:?}", x.unwrap());
        }

        assert_eq!(
            docker
                .inspect_image(&image_tag)
                .await
                .unwrap()
                .repo_tags
                .unwrap()[0],
            image_tag,
        );

        Ok(())
    }
}