Skip to main content

edgehog_device_runtime_containers/
local.rs

1// This file is part of Edgehog.
2//
3// Copyright 2025 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//! Interface with the Containers locally
20
21use std::collections::HashMap;
22
23use bollard::models::ContainerStateStatusEnum;
24use bollard::models::ContainerSummary;
25use bollard::query_parameters::ListContainersOptionsBuilder;
26use bollard::secret::{ContainerInspectResponse, ContainerStatsResponse};
27use edgehog_store::conversions::SqlUuid;
28use edgehog_store::models::containers::container::ContainerStatus;
29use tracing::trace;
30use uuid::Uuid;
31
32use crate::container::ContainerId;
33use crate::store::StateStore;
34use crate::Docker;
35
36#[cfg(feature = "__mock")]
37use crate::client::DockerTrait;
38
39/// Container handle for local clients
40#[derive(Debug, Clone)]
41pub struct ContainerHandle {
42    pub(crate) client: Docker,
43    pub(crate) store: StateStore,
44}
45
46#[cfg_attr(feature = "__mock", mockall::automock)]
47impl ContainerHandle {
48    /// Create a new container handle
49    pub fn new(client: Docker, store: StateStore) -> Self {
50        Self { client, store }
51    }
52
53    async fn list_containers(
54        &self,
55        ids: &[(SqlUuid, Option<String>)],
56        containers_status: Vec<ContainerStateStatusEnum>,
57    ) -> eyre::Result<Vec<ContainerSummary>> {
58        let local_ids = ids.iter().filter_map(|(_, local)| local.clone()).collect();
59        let containers_status = containers_status
60            .into_iter()
61            .filter_map(|s| match s {
62                ContainerStateStatusEnum::EMPTY => None,
63                ContainerStateStatusEnum::CREATED
64                | ContainerStateStatusEnum::RUNNING
65                | ContainerStateStatusEnum::PAUSED
66                | ContainerStateStatusEnum::RESTARTING
67                | ContainerStateStatusEnum::REMOVING
68                | ContainerStateStatusEnum::EXITED
69                | ContainerStateStatusEnum::DEAD => Some(s.to_string()),
70            })
71            .collect();
72
73        let filters = HashMap::from_iter([
74            ("id".to_string(), local_ids),
75            ("status".to_string(), containers_status),
76        ]);
77
78        let opt = ListContainersOptionsBuilder::new()
79            .all(true)
80            .filters(&filters)
81            .build();
82
83        let containers = self.client.list_containers(Some(opt)).await?;
84
85        Ok(containers)
86    }
87
88    /// List the container ids.
89    ///
90    /// If the container status list is empty, all the ids are returned. This includes the ones of
91    /// container that have not been created yet. Which will have a container_id unset.
92    pub async fn list_ids(
93        &self,
94        containers_status: Vec<ContainerStateStatusEnum>,
95    ) -> eyre::Result<Vec<(Uuid, Option<String>)>> {
96        let mut ids = self
97            .store
98            .load_containers_in_state(vec![ContainerStatus::Stopped, ContainerStatus::Running])
99            .await?;
100
101        if containers_status.is_empty() {
102            return Ok(ids
103                .into_iter()
104                .map(|(id, local_id)| (*id, local_id))
105                .collect());
106        }
107
108        let containers = self.list_containers(&ids, containers_status).await?;
109
110        // Used for binary searching the ids
111        ids.sort_by(|(_, a), (_, b)| a.cmp(b));
112
113        let ids = containers
114            .into_iter()
115            .filter_map(|summary| {
116                let idx = summary
117                    .id
118                    .as_ref()
119                    .and_then(|b| ids.binary_search_by(|(_, a)| a.as_ref().cmp(&Some(b))).ok())?;
120
121                let id = *ids[idx].0;
122
123                Some((id, summary.id))
124            })
125            .collect();
126
127        Ok(ids)
128    }
129
130    /// List the container
131    pub async fn list(
132        &self,
133        containers_status: Vec<ContainerStateStatusEnum>,
134    ) -> eyre::Result<HashMap<Uuid, ContainerSummary>> {
135        let mut ids = self
136            .store
137            .load_containers_in_state(vec![ContainerStatus::Stopped, ContainerStatus::Running])
138            .await?;
139
140        let containers = self.list_containers(&ids, containers_status).await?;
141
142        // Used for binary searching the ids
143        ids.sort_by(|(_, a), (_, b)| a.cmp(b));
144
145        let map = containers
146            .into_iter()
147            .filter_map(|summary| {
148                let idx = summary
149                    .id
150                    .as_ref()
151                    .and_then(|b| ids.binary_search_by(|(_, a)| a.as_ref().cmp(&Some(b))).ok())?;
152
153                let id = *ids[idx].0;
154
155                Some((id, summary))
156            })
157            .collect();
158
159        Ok(map)
160    }
161
162    /// Get all the containers
163    ///
164    /// This will list the containers and get all the information
165    pub async fn get_all(
166        &self,
167        containers_status: Vec<ContainerStateStatusEnum>,
168    ) -> eyre::Result<Vec<(Uuid, ContainerInspectResponse)>> {
169        let ids = self
170            .store
171            .load_containers_in_state(vec![ContainerStatus::Stopped, ContainerStatus::Running])
172            .await?;
173
174        let mut containers = Vec::with_capacity(ids.len());
175        for (id, local_id) in ids {
176            let container = ContainerId::new(local_id, *id)
177                .inspect(&self.client)
178                .await?;
179
180            let Some(container) = container else {
181                trace!(%id, "skipped container is missing");
182
183                continue;
184            };
185
186            let in_state = container
187                .state
188                .as_ref()
189                .and_then(|state| state.status)
190                .is_some_and(|status| {
191                    containers_status.is_empty() || containers_status.contains(&status)
192                });
193
194            if !in_state {
195                trace!(%id, "skipped container for state filter");
196
197                continue;
198            }
199
200            containers.push((*id, container));
201        }
202
203        Ok(containers)
204    }
205
206    /// Get the container
207    pub async fn get(&self, id: Uuid) -> eyre::Result<Option<ContainerInspectResponse>> {
208        let local_id = self.store.load_container_local_id(id).await?;
209
210        let container = ContainerId::new(local_id, id).inspect(&self.client).await?;
211
212        Ok(container)
213    }
214
215    /// Start the container
216    pub async fn start(&self, id: Uuid) -> eyre::Result<Option<()>> {
217        let local_id = self.store.load_container_local_id(id).await?;
218
219        let started = ContainerId::new(local_id, id).start(&self.client).await?;
220
221        Ok(started)
222    }
223
224    /// Stop the container
225    pub async fn stop(&self, id: Uuid) -> eyre::Result<Option<()>> {
226        let local_id = self.store.load_container_local_id(id).await?;
227
228        let container = ContainerId::new(local_id, id).stop(&self.client).await?;
229
230        Ok(container)
231    }
232
233    /// Stats for a container
234    pub async fn stats(&self, id: &Uuid) -> eyre::Result<Option<ContainerStatsResponse>> {
235        let local_id = self.store.load_container_local_id(*id).await?;
236
237        let container = ContainerId::new(local_id, *id).stats(&self.client).await?;
238
239        Ok(container)
240    }
241
242    /// Stats for all the containers
243    pub async fn all_stats(&self) -> eyre::Result<Vec<(Uuid, ContainerStatsResponse)>> {
244        let ids = self
245            .store
246            // Containers that have been created
247            .load_containers_in_state(vec![ContainerStatus::Stopped, ContainerStatus::Running])
248            .await?;
249
250        let mut stats = Vec::with_capacity(ids.len());
251
252        for (id, local_id) in ids {
253            if let Some(stat) = ContainerId::new(local_id, *id).stats(&self.client).await? {
254                stats.push((*id, stat));
255            }
256        }
257
258        Ok(stats)
259    }
260}