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
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
use std::collections::HashMap;

use testcontainers::{core::WaitFor, Image, ImageArgs};

const NAME: &str = "minio/minio";
const TAG: &str = "RELEASE.2022-02-07T08-17-33Z";

const DIR: &str = "/data";
const CONSOLE_ADDRESS: &str = ":9001";

#[derive(Debug)]
pub struct MinIO {
    env_vars: HashMap<String, String>,
}

impl Default for MinIO {
    fn default() -> Self {
        let mut env_vars = HashMap::new();
        env_vars.insert(
            "MINIO_CONSOLE_ADDRESS".to_owned(),
            CONSOLE_ADDRESS.to_owned(),
        );

        Self { env_vars }
    }
}

#[derive(Debug, Clone)]
pub struct MinIOServerArgs {
    pub dir: String,
    pub certs_dir: Option<String>,
    pub json_log: bool,
}

impl Default for MinIOServerArgs {
    fn default() -> Self {
        Self {
            dir: DIR.to_owned(),
            certs_dir: None,
            json_log: false,
        }
    }
}

impl ImageArgs for MinIOServerArgs {
    fn into_iterator(self) -> Box<dyn Iterator<Item = String>> {
        let mut args = vec!["server".to_owned(), self.dir.to_owned()];

        if let Some(ref certs_dir) = self.certs_dir {
            args.push("--certs-dir".to_owned());
            args.push(certs_dir.to_owned())
        }

        if self.json_log {
            args.push("--json".to_owned());
        }

        Box::new(args.into_iter())
    }
}

impl Image for MinIO {
    type Args = MinIOServerArgs;

    fn name(&self) -> String {
        NAME.to_owned()
    }

    fn tag(&self) -> String {
        TAG.to_owned()
    }

    fn ready_conditions(&self) -> Vec<WaitFor> {
        vec![WaitFor::message_on_stdout("API:")]
    }

    fn env_vars(&self) -> Box<dyn Iterator<Item = (&String, &String)> + '_> {
        Box::new(self.env_vars.iter())
    }
}

#[cfg(test)]
mod tests {
    use std::net::IpAddr;

    use aws_config::{meta::region::RegionProviderChain, BehaviorVersion};
    use aws_sdk_s3::{config::Credentials, Client};
    use testcontainers::runners::AsyncRunner;

    use crate::minio;

    #[tokio::test]
    async fn minio_buckets() {
        let minio = minio::MinIO::default();
        let node = minio.start().await;

        let host_ip = node.get_host_ip_address().await;
        let host_port = node.get_host_port_ipv4(9000).await;

        let client = build_s3_client(host_ip, host_port).await;

        let bucket_name = "test-bucket";

        client
            .create_bucket()
            .bucket(bucket_name)
            .send()
            .await
            .expect("Failed to create test bucket");

        let buckets = client
            .list_buckets()
            .send()
            .await
            .expect("Failed to get list of buckets")
            .buckets
            .unwrap();
        assert_eq!(1, buckets.len());
        assert_eq!(bucket_name, buckets[0].name.as_ref().unwrap());
    }

    async fn build_s3_client(hos_ip: IpAddr, host_port: u16) -> Client {
        let endpoint_uri = format!("http://{hos_ip}:{host_port}");
        let region_provider = RegionProviderChain::default_provider().or_else("us-east-1");
        let creds = Credentials::new("minioadmin", "minioadmin", None, None, "test");

        // Default MinIO credentials (Can be overridden by ENV container variables)
        let shared_config = aws_config::defaults(BehaviorVersion::latest())
            .region(region_provider)
            .endpoint_url(endpoint_uri)
            .credentials_provider(creds)
            .load()
            .await;

        Client::new(&shared_config)
    }
}