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
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
use super::Docker;
use crate::{DockerTestError, Image, Source};
use bollard::{
    auth::DockerCredentials, errors::Error, image::CreateImageOptions, secret::CreateImageInfo,
};
use futures::StreamExt;
use tracing::{debug, event, Level};

impl Docker {
    /// Pulls the `Image` if neccessary.
    ///
    /// This function respects the `Image` Source and PullPolicy settings.
    pub async fn pull_image(
        &self,
        image: &Image,
        default_source: &Source,
    ) -> Result<(), DockerTestError> {
        let pull_source = match &image.source {
            None => default_source,
            Some(r) => r,
        };

        let exists = self.does_image_exist(image).await?;

        if image.should_pull(exists, pull_source)? {
            let auth = image.resolve_auth(pull_source)?;
            self.do_pull(image, auth).await?;
        }

        let image_id = self.get_image_id(image).await?;

        // FIXME: If we encounter a scenario where the image should not be pulled, we need to err
        // with appropriate information. Currently, it fails with the same error message as
        // other scenarios.
        image.set_id(image_id).await;
        Ok(())
    }

    /// Checks whether the image exists locally through attempting to inspect it.
    ///
    /// If docker daemon communication failed, we will also implicitly return false.
    async fn does_image_exist(&self, image: &Image) -> Result<bool, DockerTestError> {
        match self
            .client
            .inspect_image(&format!("{}:{}", image.repository, image.tag))
            .await
        {
            Ok(_) => Ok(true),
            Err(e) => match e {
                Error::DockerResponseServerError {
                    message: _,
                    status_code,
                } => {
                    if status_code == 404 {
                        Ok(false)
                    } else {
                        Err(DockerTestError::Daemon(e.to_string()))
                    }
                }
                _ => Err(DockerTestError::Daemon(e.to_string())),
            },
        }
    }

    // Pulls the image from its source
    // NOTE(lint): uncertain how to structure this otherwise
    #[allow(clippy::match_single_binding)]
    async fn do_pull(
        &self,
        image: &Image,
        auth: Option<DockerCredentials>,
    ) -> Result<(), DockerTestError> {
        debug!("pulling image: {}:{}", image.repository, image.tag);
        let options = Some(CreateImageOptions::<&str> {
            from_image: &image.repository,
            tag: &image.tag,
            ..Default::default()
        });

        let mut stream = self.client.create_image(options, None, auth);
        // This stream will intermittently yield a progress update.
        while let Some(result) = stream.next().await {
            match result {
                Ok(intermitten_result) => match intermitten_result {
                    CreateImageInfo {
                        id,
                        error,
                        error_detail,
                        status,
                        progress,
                        progress_detail,
                    } => {
                        if error.is_some() {
                            event!(
                                Level::ERROR,
                                "pull error {} {:?}",
                                error.clone().unwrap(),
                                error_detail.unwrap_or_default()
                            );
                        } else {
                            event!(
                                Level::TRACE,
                                "pull progress {} {:?} {:?} {:?}",
                                status.clone().unwrap_or_default(),
                                id.clone().unwrap_or_default(),
                                progress.clone().unwrap_or_default(),
                                progress_detail.clone().unwrap_or_default()
                            );
                        }
                    }
                },
                Err(e) => {
                    let msg = match e {
                        Error::DockerResponseServerError {
                            message: _,
                            status_code,
                        } => {
                            if status_code == 404 {
                                "unknown registry or image".to_string()
                            } else {
                                e.to_string()
                            }
                        }
                        _ => e.to_string(),
                    };
                    return Err(DockerTestError::Pull {
                        repository: image.repository.to_string(),
                        tag: image.tag.to_string(),
                        error: msg,
                    });
                }
            }
        }

        // TODO: Verify that we have actually pulled the image.
        // NOTE: The engine may return a 500 when we unauthorized, but bollard does not give us
        // this failure. Rather, it just does not provide anthing in the stream.
        // If a repo is submitted that we do not have access to, and no auth is supplied,
        // we will no error.

        event!(Level::DEBUG, "successfully pulled image");
        Ok(())
    }

    async fn get_image_id(&self, image: &Image) -> Result<String, DockerTestError> {
        match self
            .client
            .inspect_image(&format!("{}:{}", image.repository, image.tag))
            .await
        {
            Ok(details) => Ok(details.id.expect("image did not have an id")),
            Err(e) => {
                event!(
                    Level::TRACE,
                    "failed to retrieve ID of image: {}, tag: {}, source: {:?} ",
                    image.repository,
                    image.tag,
                    image.source
                );
                Err(DockerTestError::Pull {
                    repository: image.repository.to_string(),
                    tag: image.tag.to_string(),
                    error: e.to_string(),
                })
            }
        }
    }
}