dockerfile_build/
lib.rs

1mod utils;
2use std::fmt;
3
4use bollard::{image::BuildImageOptions, service::BuildInfo, Docker};
5use futures::stream::StreamExt;
6use thiserror::Error;
7use tracing::{event, Level};
8
9use crate::utils::tarball;
10
11#[derive(Error, Debug)]
12pub enum DockerfileError<'a> {
13    #[error("Building image via Docker API failed: DockerfileImage: {dockerfile_image}")]
14    BuildImage {
15        error: String,
16        dockerfile_image: &'a DockerfileImage,
17    },
18}
19
20pub struct DockerfileImage {
21    repository: String,
22    tag: String,
23    path: String,
24    name: String,
25    #[cfg(feature = "dockertest")]
26    image: dockertest::Image,
27}
28
29impl std::fmt::Debug for DockerfileImage {
30    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
31        f.debug_struct("DockerfileImage")
32            .field("repository", &self.repository)
33            .field("tag", &self.tag)
34            .field("path", &self.path)
35            .field("name", &self.name)
36            .field("image", &self.to_string())
37            .finish()
38    }
39}
40
41impl fmt::Display for DockerfileImage {
42    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
43        write!(
44            f,
45            "repository: {}, tag: {}, path: {}, name: {}",
46            self.repository, self.tag, self.path, self.name
47        )
48    }
49}
50
51impl DockerfileImage {
52    pub fn with_dockerfile<T: ToString>(
53        repository: T,
54        tag: Option<T>,
55        path: Option<T>,
56        name: Option<T>,
57    ) -> DockerfileImage {
58        DockerfileImage {
59            repository: repository.to_string(),
60            tag: tag.map_or("latest".to_string(), |tag| tag.to_string()),
61            path: path.map_or("./dockerfile".to_string(), |path| path.to_string()),
62            name: name.map_or("Dockerfile".to_string(), |name| name.to_string()),
63            #[cfg(feature = "dockertest")]
64            image: dockertest::Image::with_repository(repository),
65        }
66    }
67
68    #[cfg(feature = "dockertest")]
69    pub(crate) fn image(&self) -> &dockertest::Image {
70        &self.image
71    }
72
73    pub async fn build(&self, client: &Docker) -> Result<(), DockerfileError> {
74        event!(
75            Level::INFO,
76            "building image: {}:{}",
77            &self.repository,
78            &self.tag
79        );
80        let options = BuildImageOptions::<&str> {
81            dockerfile: &self.name,
82            t: &format!("{}:{}", &self.repository, &self.tag), // This is the tag we would give the image when building, docker build . -t <name:tag>
83            rm: true,
84            ..Default::default()
85        };
86
87        let buf = tarball(&self.path, &self.name).unwrap();
88
89        let mut stream = client.build_image(options, None, Some(buf.into()));
90        while let Some(result) = stream.next().await {
91            match result {
92                Ok(intermitten_result) => {
93                    let BuildInfo {
94                        id,
95                        stream: _,
96                        error,
97                        error_detail,
98                        status,
99                        progress,
100                        progress_detail,
101                        aux: _,
102                    } = intermitten_result;
103                    if error.is_some() {
104                        event!(
105                            Level::ERROR,
106                            "build error {} {:?}",
107                            error.clone().unwrap(),
108                            error_detail.clone().unwrap()
109                        );
110                    } else {
111                        event!(
112                            Level::TRACE,
113                            "build progress {} {:?} {:?} {:?}",
114                            status.clone().unwrap_or_default(),
115                            id.clone().unwrap_or_default(),
116                            progress.clone().unwrap_or_default(),
117                            progress_detail.clone().unwrap_or_default(),
118                        );
119                    };
120                }
121                Err(e) => {
122                    let msg = e.to_string();
123                    return Err(DockerfileError::BuildImage {
124                        error: msg,
125                        dockerfile_image: self,
126                    });
127                }
128            }
129        }
130
131        event!(Level::DEBUG, "successfully built image");
132        Ok(())
133    }
134}
135
136#[cfg(test)]
137mod tests {
138    use bollard::Docker;
139    use wiremock::{matchers::method, matchers::path, Mock, MockServer, ResponseTemplate};
140
141    use crate::DockerfileImage;
142
143    #[tokio::test]
144    async fn it_works() {
145        let mock_server = MockServer::start().await;
146
147        Mock::given(method("POST"))
148            .and(path("/build"))
149            .respond_with(ResponseTemplate::new(200))
150            .expect(1..)
151            .mount(&mock_server)
152            .await;
153        let client =
154            Docker::connect_with_http(&mock_server.uri(), 4, bollard::API_DEFAULT_VERSION).unwrap();
155
156        let img = DockerfileImage::with_dockerfile(
157            "dockertest-dockerfile/hello",
158            None,
159            Some("./dockerfiles/hello.dockerfile"),
160            None,
161        );
162        img.build(&client).await.unwrap();
163
164        let received_requests = mock_server.received_requests().await.unwrap();
165        let request = received_requests.get(0).unwrap();
166        dbg!(request);
167        let mut params = request.url.query_pairs();
168        assert!(
169            params.any(|x| x.0.eq("t")
170                && x.1.contains("dockertest-dockerfile")
171                && x.1.contains("hello")
172                && x.1.contains("latest")),
173            "failure checking build image request contains tag"
174        );
175    }
176
177    #[tokio::test]
178    async fn it_works_custom_tag() {
179        let mock_server = MockServer::start().await;
180
181        Mock::given(method("POST"))
182            .and(path("/build"))
183            .respond_with(ResponseTemplate::new(200))
184            .expect(1..)
185            .mount(&mock_server)
186            .await;
187        let client =
188            Docker::connect_with_http(&mock_server.uri(), 4, bollard::API_DEFAULT_VERSION).unwrap();
189
190        let img = DockerfileImage::with_dockerfile(
191            "dockertest-dockerfile/hello",
192            Some("stable"),
193            Some("./dockerfiles/hello.dockerfile"),
194            None,
195        );
196        img.build(&client).await.unwrap();
197
198        let received_requests = mock_server.received_requests().await.unwrap();
199        let request = received_requests.get(0).unwrap();
200        dbg!(request);
201        let mut params = request.url.query_pairs();
202        assert!(
203            params.any(|x| x.0.eq("t")
204                && x.1.contains("dockertest-dockerfile")
205                && x.1.contains("hello")
206                && x.1.contains("stable")),
207            "failure checking build image request contains tag"
208        );
209    }
210
211    #[tokio::test]
212    async fn handle_error() {
213        let mock_server = MockServer::start().await;
214
215        Mock::given(method("POST"))
216            .and(path("/build"))
217            .respond_with(
218                ResponseTemplate::new(400)
219                    .set_body_string(r#"[{"message":"Something went wrong!"}]"#),
220            )
221            .expect(1..)
222            .mount(&mock_server)
223            .await;
224        let client =
225            Docker::connect_with_http(&mock_server.uri(), 4, bollard::API_DEFAULT_VERSION).unwrap();
226
227        let img = DockerfileImage::with_dockerfile(
228            "dockertest-dockerfile/hello",
229            Some("stable"),
230            Some("./dockerfiles/hello.dockerfile"),
231            None,
232        );
233        img.build(&client)
234            .await
235            .expect_err("Expected DockerfileError::BuildImage");
236    }
237}