edgehog_device_runtime_containers/
local.rs1use 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#[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 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 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 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 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 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 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 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 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 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 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 pub async fn all_stats(&self) -> eyre::Result<Vec<(Uuid, ContainerStatsResponse)>> {
244 let ids = self
245 .store
246 .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}