Skip to main content

edgehog_device_runtime_containers/requests/
mod.rs

1// This file is part of Edgehog.
2//
3// Copyright 2024 - 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//! Container requests sent from Astarte.
20
21use std::{borrow::Borrow, fmt::Display, num::ParseIntError, ops::Deref};
22
23use astarte_device_sdk::{
24    event::FromEventError, types::TypeError, AstarteData, DeviceEvent, FromEvent,
25};
26use container::{CreateContainer, RestartPolicyError};
27use deployment::{CreateDeployment, DeploymentCommand, DeploymentUpdate};
28use tracing::error;
29use uuid::Uuid;
30
31use self::device_mapping::CreateDeviceMapping;
32use self::{image::CreateImage, network::CreateNetwork, volume::CreateVolume};
33
34pub mod container;
35pub mod deployment;
36pub mod device_mapping;
37pub mod image;
38pub mod network;
39pub mod volume;
40
41/// Error from handling the Astarte request.
42#[non_exhaustive]
43#[derive(Debug, displaydoc::Display, thiserror::Error)]
44pub enum ReqError {
45    /// couldn't parse option, expected key=value but got {0}
46    Option(String),
47    /// couldn't parse container restart policy
48    RestartPolicy(#[from] RestartPolicyError),
49    /// couldn't parse port binding
50    PortBinding(#[from] BindingError),
51}
52
53/// Error from parsing a binding
54#[non_exhaustive]
55#[derive(Debug, displaydoc::Display, thiserror::Error)]
56pub enum BindingError {
57    /// couldn't parse {binding} port {value}
58    Port {
59        /// Binding received
60        binding: &'static str,
61        /// Port of the binding
62        value: String,
63        /// Error converting the port
64        #[source]
65        source: ParseIntError,
66    },
67    /// couldn't convert index for storage
68    Idx,
69}
70
71/// Create request from Astarte.
72#[derive(Debug, Clone, PartialEq)]
73pub enum ContainerRequest {
74    /// Request to create an image.
75    Image(CreateImage),
76    /// Request to create a volume.
77    Volume(CreateVolume),
78    /// Request to create a network.
79    Network(CreateNetwork),
80    /// Request to create a device mapping.
81    DeviceMapping(CreateDeviceMapping),
82    /// Request to create a container.
83    Container(Box<CreateContainer>),
84    /// Request to create a deployment.
85    Deployment(CreateDeployment),
86    /// Command for a deployment
87    DeploymentCommand(DeploymentCommand),
88    /// Update between two deployments
89    DeploymentUpdate(DeploymentUpdate),
90}
91
92impl ContainerRequest {
93    pub(crate) fn deployment_id(&self) -> Uuid {
94        match self {
95            ContainerRequest::Image(value) => value.deployment_id.0,
96            ContainerRequest::Volume(value) => value.deployment_id.0,
97            ContainerRequest::Network(value) => value.deployment_id.0,
98            ContainerRequest::Container(value) => value.deployment_id.0,
99            ContainerRequest::DeviceMapping(dm) => dm.deployment_id.0,
100            ContainerRequest::Deployment(create_deployment) => create_deployment.id.0,
101            ContainerRequest::DeploymentCommand(deployment_command) => deployment_command.id,
102            ContainerRequest::DeploymentUpdate(deployment_update) => deployment_update.from,
103        }
104    }
105}
106
107impl FromEvent for ContainerRequest {
108    type Err = FromEventError;
109
110    fn from_event(value: DeviceEvent) -> Result<Self, Self::Err> {
111        match value.interface.as_str() {
112            "io.edgehog.devicemanager.apps.CreateImageRequest" => {
113                CreateImage::from_event(value).map(ContainerRequest::Image)
114            }
115            "io.edgehog.devicemanager.apps.CreateVolumeRequest" => {
116                CreateVolume::from_event(value).map(ContainerRequest::Volume)
117            }
118            "io.edgehog.devicemanager.apps.CreateNetworkRequest" => {
119                CreateNetwork::from_event(value).map(ContainerRequest::Network)
120            }
121            "io.edgehog.devicemanager.apps.CreateDeviceMappingRequest" => {
122                CreateDeviceMapping::from_event(value).map(ContainerRequest::DeviceMapping)
123            }
124            "io.edgehog.devicemanager.apps.CreateContainerRequest" => {
125                CreateContainer::from_event(value)
126                    .map(|value| ContainerRequest::Container(Box::new(value)))
127            }
128            "io.edgehog.devicemanager.apps.CreateDeploymentRequest" => {
129                CreateDeployment::from_event(value).map(ContainerRequest::Deployment)
130            }
131            "io.edgehog.devicemanager.apps.DeploymentCommand" => {
132                DeploymentCommand::from_event(value).map(ContainerRequest::DeploymentCommand)
133            }
134            "io.edgehog.devicemanager.apps.DeploymentUpdate" => {
135                DeploymentUpdate::from_event(value).map(ContainerRequest::DeploymentUpdate)
136            }
137            _ => Err(FromEventError::Interface(value.interface.clone())),
138        }
139    }
140}
141
142/// Wrapper to convert an [`AstarteData`] to [`Uuid`].
143#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
144pub(crate) struct ReqUuid(pub(crate) Uuid);
145
146impl Display for ReqUuid {
147    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
148        write!(f, "{}", self.0)
149    }
150}
151
152impl Borrow<Uuid> for ReqUuid {
153    fn borrow(&self) -> &Uuid {
154        &self.0
155    }
156}
157
158impl Deref for ReqUuid {
159    type Target = Uuid;
160
161    fn deref(&self) -> &Self::Target {
162        &self.0
163    }
164}
165
166impl TryFrom<&str> for ReqUuid {
167    type Error = TypeError;
168
169    fn try_from(value: &str) -> Result<Self, Self::Error> {
170        Uuid::parse_str(value).map(ReqUuid).map_err(|err| {
171            error!(
172                error = format!("{:#}", eyre::Report::new(err)),
173                value, "couldn't parse uuid value"
174            );
175
176            TypeError::Conversion {
177                ctx: format!("couldn't parse uuid value: {value}"),
178            }
179        })
180    }
181}
182
183impl TryFrom<AstarteData> for ReqUuid {
184    type Error = TypeError;
185
186    fn try_from(value: AstarteData) -> Result<Self, Self::Error> {
187        let value = String::try_from(value)?;
188
189        Self::try_from(value.as_str())
190    }
191}
192
193impl From<ReqUuid> for Uuid {
194    fn from(value: ReqUuid) -> Self {
195        value.0
196    }
197}
198
199impl From<&ReqUuid> for Uuid {
200    fn from(value: &ReqUuid) -> Self {
201        value.0
202    }
203}
204
205/// Wrapper to convert an [`AstarteData`] to [`Vec<Uuid>`].
206///
207/// This is required because we cannot implement [`TryFrom<AstarteData>`] for [`Vec<ReqUuid>`], because
208/// of the orphan rule.
209#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Default)]
210pub(crate) struct VecReqUuid(pub(crate) Vec<ReqUuid>);
211
212impl Deref for VecReqUuid {
213    type Target = Vec<ReqUuid>;
214
215    fn deref(&self) -> &Self::Target {
216        &self.0
217    }
218}
219
220impl TryFrom<AstarteData> for VecReqUuid {
221    type Error = TypeError;
222
223    fn try_from(value: AstarteData) -> Result<Self, Self::Error> {
224        let value = Vec::<String>::try_from(value)?;
225
226        value
227            .iter()
228            .map(|v| ReqUuid::try_from(v.as_str()))
229            .collect::<Result<Vec<ReqUuid>, TypeError>>()
230            .map(VecReqUuid)
231    }
232}
233
234/// Optional non empty string
235#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
236pub struct OptString(Option<String>);
237
238impl TryFrom<AstarteData> for OptString {
239    type Error = TypeError;
240
241    fn try_from(value: AstarteData) -> Result<Self, Self::Error> {
242        let value = String::try_from(value)?;
243
244        Ok(Self::from(value))
245    }
246}
247
248impl From<String> for OptString {
249    fn from(value: String) -> Self {
250        if value.is_empty() {
251            OptString(None)
252        } else {
253            OptString(Some(value))
254        }
255    }
256}
257
258impl From<OptString> for Option<String> {
259    fn from(value: OptString) -> Self {
260        value.0
261    }
262}
263
264#[cfg(test)]
265mod tests {
266    use super::*;
267
268    use image::tests::create_image_request_event;
269    use network::{tests::create_network_request_event, CreateNetwork};
270    use pretty_assertions::assert_eq;
271
272    use crate::requests::ContainerRequest;
273
274    #[test]
275    fn from_event_image() {
276        let id = Uuid::new_v4();
277        let deployment_id = Uuid::new_v4();
278
279        let event = create_image_request_event(id, deployment_id, "reference", "registry_auth");
280
281        let request = ContainerRequest::from_event(event).unwrap();
282
283        let expect = ContainerRequest::Image(CreateImage {
284            id: ReqUuid(id),
285            deployment_id: ReqUuid(deployment_id),
286            reference: "reference".to_string(),
287            registry_auth: "registry_auth".to_string(),
288        });
289
290        assert_eq!(request, expect);
291    }
292
293    #[test]
294    fn from_event_network() {
295        let id = Uuid::new_v4();
296        let deployment_id = Uuid::new_v4();
297        let event = create_network_request_event(id, deployment_id, "driver", &[]);
298
299        let request = ContainerRequest::from_event(event).unwrap();
300
301        let expect = CreateNetwork {
302            id: ReqUuid(id),
303            deployment_id: ReqUuid(deployment_id),
304            driver: "driver".to_string(),
305            internal: false,
306            enable_ipv6: false,
307            options: Vec::new(),
308        };
309
310        assert_eq!(request, ContainerRequest::Network(expect));
311    }
312
313    #[test]
314    fn optional_string() {
315        let cases = [
316            ("", OptString(None)),
317            ("some", OptString(Some("some".to_string()))),
318        ];
319
320        for (case, exp) in cases {
321            let res = OptString::try_from(AstarteData::from(case)).unwrap();
322
323            assert_eq!(res, exp);
324        }
325    }
326}