Skip to main content

edgehog_device_runtime_containers/store/
device_mapping.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
19use diesel::query_dsl::methods::{FilterDsl, SelectDsl};
20use diesel::{delete, insert_or_ignore_into, update, ExpressionMethods, RunQueryDsl};
21use edgehog_store::conversions::SqlUuid;
22use edgehog_store::db::HandleError;
23use edgehog_store::models::containers::container::ContainerMissingDeviceMapping;
24use edgehog_store::models::containers::device_mapping::DeviceMapping;
25use edgehog_store::models::containers::device_mapping::DeviceMappingStatus;
26use edgehog_store::models::QueryModel;
27use edgehog_store::schema::containers::{container_device_mappings, device_mappings};
28use tracing::instrument;
29use uuid::Uuid;
30
31use crate::requests::device_mapping::CreateDeviceMapping;
32
33use super::{Result, StateStore};
34
35impl StateStore {
36    /// Stores the device mapping received from the CreateRequest
37    #[instrument(skip_all, fields(%create_device_mapping.id))]
38    pub(crate) async fn create_device_mapping(
39        &self,
40        create_device_mapping: CreateDeviceMapping,
41    ) -> Result<()> {
42        let dm_value = DeviceMapping::from(create_device_mapping);
43
44        self.handle
45            .for_write(move |writer| {
46                insert_or_ignore_into(device_mappings::table)
47                    .values(&dm_value)
48                    .execute(writer)?;
49
50                insert_or_ignore_into(container_device_mappings::table)
51                    .values(ContainerMissingDeviceMapping::find_by_device_mapping(
52                        &dm_value.id,
53                    ))
54                    .execute(writer)?;
55
56                delete(ContainerMissingDeviceMapping::find_by_device_mapping(
57                    &dm_value.id,
58                ))
59                .execute(writer)?;
60
61                Ok(())
62            })
63            .await?;
64
65        Ok(())
66    }
67
68    /// Updates the state of a device_mapping
69    #[instrument(skip(self))]
70    pub(crate) async fn update_device_mapping_status(
71        &self,
72        device_mapping_id: Uuid,
73        status: DeviceMappingStatus,
74    ) -> Result<()> {
75        self.handle
76            .for_write(move |writer| {
77                let updated = update(DeviceMapping::find_id(&SqlUuid::new(device_mapping_id)))
78                    .set(device_mappings::status.eq(status))
79                    .execute(writer)?;
80
81                HandleError::check_modified(updated, 1)?;
82
83                Ok(())
84            })
85            .await?;
86
87        Ok(())
88    }
89
90    /// Deletes a [`DeviceMapping`]
91    #[instrument(skip(self))]
92    pub(crate) async fn delete_device_mapping(&self, device_mapping_id: Uuid) -> Result<()> {
93        self.handle
94            .for_write(move |writer| {
95                let updated = delete(DeviceMapping::find_id(&SqlUuid::new(device_mapping_id)))
96                    .execute(writer)?;
97
98                HandleError::check_modified(updated, 1)?;
99
100                Ok(())
101            })
102            .await?;
103
104        Ok(())
105    }
106
107    #[instrument(skip(self))]
108    pub(crate) async fn load_device_mappings_to_publish(&self) -> Result<Vec<SqlUuid>> {
109        let device_mappings = self
110            .handle
111            .for_read(move |reader| {
112                let device_mappings = device_mappings::table
113                    .select(device_mappings::id)
114                    .filter(device_mappings::status.eq(DeviceMappingStatus::Received))
115                    .load::<SqlUuid>(reader)?;
116
117                Ok(device_mappings)
118            })
119            .await?;
120
121        Ok(device_mappings)
122    }
123}
124
125impl From<CreateDeviceMapping> for DeviceMapping {
126    fn from(
127        CreateDeviceMapping {
128            id,
129            deployment_id: _,
130            path_on_host,
131            path_in_container,
132            c_group_permissions,
133        }: CreateDeviceMapping,
134    ) -> Self {
135        Self {
136            id: SqlUuid::new(id),
137            status: DeviceMappingStatus::default(),
138            path_on_host,
139            path_in_container,
140            cgroup_permissions: c_group_permissions.into(),
141        }
142    }
143}
144
145impl From<DeviceMapping> for crate::docker::container::DeviceMapping {
146    fn from(
147        DeviceMapping {
148            id: _,
149            status: _,
150            path_on_host,
151            path_in_container,
152            cgroup_permissions,
153        }: DeviceMapping,
154    ) -> Self {
155        Self {
156            path_on_host,
157            path_in_container,
158            cgroup_permissions,
159        }
160    }
161}
162
163#[cfg(test)]
164mod tests {
165    use crate::requests::{OptString, ReqUuid};
166
167    use super::*;
168
169    use diesel::OptionalExtension;
170    use edgehog_store::db;
171    use pretty_assertions::assert_eq;
172    use tempfile::TempDir;
173
174    async fn find_device_mapping(store: &StateStore, id: Uuid) -> Option<DeviceMapping> {
175        store
176            .handle
177            .for_read(move |reader| {
178                DeviceMapping::find_id(&SqlUuid::new(id))
179                    .first::<DeviceMapping>(reader)
180                    .optional()
181                    .map_err(HandleError::Query)
182            })
183            .await
184            .unwrap()
185    }
186
187    #[tokio::test]
188    async fn should_store() {
189        let tmp = TempDir::with_prefix("store_device_mapping").unwrap();
190        let db_file = tmp.path().join("state.db");
191        let db_file = db_file.to_str().unwrap();
192
193        let handle = db::Handle::open(db_file).await.unwrap();
194        let store = StateStore::new(handle);
195
196        let device_mapping_id = Uuid::new_v4();
197        let deployment_id = Uuid::new_v4();
198        let device_mapping = CreateDeviceMapping {
199            id: ReqUuid(device_mapping_id),
200            deployment_id: ReqUuid(deployment_id),
201            path_on_host: "/dev/tty12".to_string(),
202            path_in_container: "/dev/tty12".to_string(),
203            c_group_permissions: OptString::from("mvr".to_string()),
204        };
205        store.create_device_mapping(device_mapping).await.unwrap();
206
207        let res = find_device_mapping(&store, device_mapping_id)
208            .await
209            .unwrap();
210
211        let exp = DeviceMapping {
212            id: SqlUuid::new(device_mapping_id),
213            status: DeviceMappingStatus::Received,
214            path_on_host: "/dev/tty12".to_string(),
215            path_in_container: "/dev/tty12".to_string(),
216            cgroup_permissions: Some("mvr".to_string()),
217        };
218
219        assert_eq!(res, exp);
220    }
221
222    #[tokio::test]
223    async fn should_update() {
224        let tmp = TempDir::with_prefix("update_device_mapping").unwrap();
225        let db_file = tmp.path().join("state.db");
226        let db_file = db_file.to_str().unwrap();
227
228        let handle = db::Handle::open(db_file).await.unwrap();
229        let store = StateStore::new(handle);
230
231        let device_mapping_id = Uuid::new_v4();
232        let deployment_id = Uuid::new_v4();
233        let device_mapping = CreateDeviceMapping {
234            id: ReqUuid(device_mapping_id),
235            deployment_id: ReqUuid(deployment_id),
236            path_on_host: "/dev/tty12".to_string(),
237            path_in_container: "/dev/tty12".to_string(),
238            c_group_permissions: OptString::from("mvr".to_string()),
239        };
240        store.create_device_mapping(device_mapping).await.unwrap();
241
242        store
243            .update_device_mapping_status(device_mapping_id, DeviceMappingStatus::Published)
244            .await
245            .unwrap();
246
247        let res = find_device_mapping(&store, device_mapping_id)
248            .await
249            .unwrap();
250
251        let exp = DeviceMapping {
252            id: SqlUuid::new(device_mapping_id),
253            status: DeviceMappingStatus::Published,
254            path_on_host: "/dev/tty12".to_string(),
255            path_in_container: "/dev/tty12".to_string(),
256            cgroup_permissions: Some("mvr".to_string()),
257        };
258
259        assert_eq!(res, exp);
260    }
261}