Skip to main content

edgehog_device_runtime_containers/docker/
mod.rs

1// This file is part of Edgehog.
2//
3// Copyright 2023-2024 SECO Mind Srl
4//
5// Licensed under the Apache License, Version 2.0 (the "License");
6// you may not use this file except in compliance with the License.
7// You may obtain a copy of the License at
8//
9//   http://www.apache.org/licenses/LICENSE-2.0
10//
11// Unless required by applicable law or agreed to in writing, software
12// distributed under the License is distributed on an "AS IS" BASIS,
13// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14// See the License for the specific language governing permissions and
15// limitations under the License.
16//
17// SPDX-License-Identifier: Apache-2.0
18
19//! Wrapper around the Docker client
20
21use std::{
22    borrow::{Borrow, BorrowMut},
23    ops::{Deref, DerefMut},
24};
25
26use bollard::{models::EventMessage, query_parameters::EventsOptionsBuilder};
27use futures::{Stream, TryStreamExt};
28
29pub(crate) use crate::client::*;
30use crate::error::DockerError;
31
32pub(crate) mod container;
33pub(crate) mod image;
34pub(crate) mod network;
35pub(crate) mod volume;
36
37/// Docker container manager
38#[derive(Debug, Clone)]
39pub struct Docker {
40    pub(crate) client: Client,
41}
42
43impl Docker {
44    cfg_if::cfg_if! {
45        if #[cfg(feature = "__mock")] {
46            /// Create a new Docker container manager
47            #[cfg(feature = "__mock")]
48            pub async fn connect() -> Result<Self, DockerError> {
49                let client = Client::new();
50
51                Ok(Self { client })
52            }
53        } else {
54            /// Create a new Docker container manager
55            pub async fn connect() -> Result<Self, DockerError> {
56                let client = Client::connect_with_local_defaults()
57                    .map_err(DockerError::Connection)?
58                    .negotiate_version()
59                    .await
60                    .map_err(DockerError::Version)?;
61
62                Ok(Self { client })
63            }
64        }
65    }
66
67    /// Ping the Docker daemon
68    pub async fn ping(&self) -> Result<(), DockerError> {
69        // Discard the result since it returns the string `OK`
70        self.client.ping().await.map_err(DockerError::Ping)?;
71
72        Ok(())
73    }
74
75    /// Ping the Docker daemon
76    pub fn events(&self) -> impl Stream<Item = Result<EventMessage, DockerError>> {
77        let types = ["container", "image", "volume", "network"]
78            .map(str::to_string)
79            .to_vec();
80
81        let filters = [("type".to_string(), types)].into_iter().collect();
82
83        let options = EventsOptionsBuilder::new().filters(&filters).build();
84
85        // Discard the result since it returns the string `OK`
86        self.client
87            .events(Some(options))
88            .map_err(DockerError::Events)
89    }
90}
91
92impl From<Client> for Docker {
93    fn from(client: Client) -> Self {
94        Self { client }
95    }
96}
97
98impl Borrow<Client> for Docker {
99    fn borrow(&self) -> &Client {
100        &self.client
101    }
102}
103
104impl BorrowMut<Client> for Docker {
105    fn borrow_mut(&mut self) -> &mut Client {
106        &mut self.client
107    }
108}
109
110impl Deref for Docker {
111    type Target = Client;
112
113    fn deref(&self) -> &Self::Target {
114        &self.client
115    }
116}
117
118impl DerefMut for Docker {
119    fn deref_mut(&mut self) -> &mut Self::Target {
120        &mut self.client
121    }
122}
123
124#[cfg(test)]
125pub(crate) mod tests {
126    use super::*;
127
128    /// Returns a [Docker] instance, or a mocked version with the expect statements if the mock
129    /// feature is enabled.
130    #[macro_export]
131    macro_rules! docker_mock {
132        ($mock:expr) => {{
133            #[cfg(feature = "__mock")]
134            let docker: $crate::Docker = {
135                let mock: $crate::client::Client = $mock;
136
137                Docker::from(mock)
138            };
139
140            #[cfg(not(feature = "__mock"))]
141            let docker: $crate::docker::Docker = $crate::docker::Docker::connect().unwrap();
142
143            docker
144        }};
145        ($default:expr, $mock:expr) => {{
146            #[cfg(feature = "__mock")]
147            let client: $crate::client::Client = $mock;
148
149            #[cfg(not(feature = "__mock"))]
150            let client: $crate::client::Client = $default;
151
152            $crate::Docker::from(client)
153        }};
154    }
155
156    #[cfg(feature = "__mock")]
157    pub(crate) fn not_found_response() -> bollard::errors::Error {
158        bollard::errors::Error::DockerResponseServerError {
159            status_code: 404,
160            message: "not found".to_string(),
161        }
162    }
163
164    #[tokio::test]
165    async fn test_ping_and_macro() {
166        let docker = docker_mock!(Client::connect_with_local_defaults().unwrap(), {
167            let mut mock = Client::new();
168
169            mock.expect_ping().returning(|| Ok(Default::default()));
170
171            mock
172        });
173
174        let res = docker.ping().await;
175
176        assert!(res.is_ok(), "Ping failed: {res:?}");
177    }
178}