1use std::str::FromStr;
22
23use astarte_device_sdk::FromEvent;
24use bollard::models::RestartPolicyNameEnum;
25use tracing::{instrument, trace};
26
27use crate::{container::Binding, requests::BindingError};
28
29use super::{OptString, ReqUuid, VecReqUuid};
30
31#[derive(Debug, thiserror::Error, displaydoc::Display, PartialEq)]
33pub struct RestartPolicyError {
34 value: String,
35}
36
37#[derive(Debug, Clone, FromEvent, PartialEq, Eq, PartialOrd, Ord)]
39#[from_event(
40 interface = "io.edgehog.devicemanager.apps.CreateContainerRequest",
41 path = "/container",
42 rename_all = "camelCase",
43 aggregation = "object"
44)]
45pub struct CreateContainer {
46 pub(crate) id: ReqUuid,
47 pub(crate) deployment_id: ReqUuid,
48 pub(crate) image_id: ReqUuid,
49 pub(crate) network_ids: VecReqUuid,
50 pub(crate) volume_ids: VecReqUuid,
51 pub(crate) device_mapping_ids: VecReqUuid,
52 pub(crate) hostname: String,
53 pub(crate) restart_policy: String,
54 pub(crate) env: Vec<String>,
55 pub(crate) binds: Vec<String>,
56 pub(crate) network_mode: String,
57 pub(crate) port_bindings: Vec<String>,
58 pub(crate) extra_hosts: Vec<String>,
59 pub(crate) cap_add: Vec<String>,
60 pub(crate) cap_drop: Vec<String>,
61 pub(crate) cpu_period: i64,
62 pub(crate) cpu_quota: i64,
63 pub(crate) cpu_realtime_period: i64,
64 pub(crate) cpu_realtime_runtime: i64,
65 pub(crate) memory: i64,
66 pub(crate) memory_reservation: i64,
67 pub(crate) memory_swap: i64,
68 pub(crate) memory_swappiness: i32,
69 pub(crate) volume_driver: OptString,
70 pub(crate) storage_opt: Vec<String>,
71 pub(crate) read_only_rootfs: bool,
72 pub(crate) tmpfs: Vec<String>,
73 pub(crate) privileged: bool,
74}
75
76#[derive(Debug, PartialEq, Eq)]
77pub(crate) struct ParsedBind<'a> {
78 port: u16,
79 proto: Option<&'a str>,
80 pub(crate) host: Binding<&'a str>,
81}
82
83impl<'a> ParsedBind<'a> {
84 fn new(
85 port: u16,
86 proto: Option<&'a str>,
87 host_ip: Option<&'a str>,
88 host_port: Option<u16>,
89 ) -> Self {
90 Self {
91 port,
92 proto,
93 host: Binding { host_ip, host_port },
94 }
95 }
96
97 pub(crate) fn id(&self) -> String {
98 let proto = self.proto.unwrap_or("tcp");
99
100 format!("{}/{}", self.port, proto)
101 }
102}
103
104#[instrument]
110pub(crate) fn parse_port_binding(input: &str) -> Result<ParsedBind<'_>, BindingError> {
111 let (host_ip, host_port, rest) = parse_host_ip_port(input)?;
112
113 let (container_port, protocol) = rest.split_once('/').map_or_else(
114 || {
115 trace!("container port {rest}");
116
117 (rest, None)
118 },
119 |(port, proto)| {
120 trace!("container port {port} and protocol {proto}");
121
122 (port, Some(proto))
123 },
124 );
125
126 let container_port = container_port.parse().map_err(|err| BindingError::Port {
127 binding: "container",
128 value: container_port.to_string(),
129 source: err,
130 })?;
131
132 Ok(ParsedBind::new(
133 container_port,
134 protocol,
135 host_ip,
136 host_port,
137 ))
138}
139
140#[instrument]
141fn parse_host_ip_port(input: &str) -> Result<(Option<&str>, Option<u16>, &str), BindingError> {
142 let Some((ip_or_port, rest)) = input.split_once(':') else {
143 trace!("missing host ip or port, returning rest: {input}");
144
145 return Ok((None, None, input));
146 };
147
148 match rest.split_once(':') {
149 Some((port, rest)) => {
150 let port: u16 = port.parse().map_err(|err| BindingError::Port {
151 binding: "host",
152 value: port.to_string(),
153 source: err,
154 })?;
155
156 trace!("found ip {ip_or_port} and port {port}");
157
158 Ok((Some(ip_or_port), Some(port), rest))
159 }
160 None => {
161 if let Ok(port) = ip_or_port.parse::<u16>() {
163 trace!("found port {port}");
164
165 Ok((None, Some(port), rest))
166 } else {
167 trace!("found ip {ip_or_port}");
168 Ok((Some(ip_or_port), None, rest))
169 }
170 }
171 }
172}
173
174#[derive(Debug, Clone, Copy, PartialEq, Eq)]
175pub(crate) enum RestartPolicy {
176 Empty,
177 No,
178 Always,
179 UnlessStopped,
180 OnFailure,
181}
182
183impl FromStr for RestartPolicy {
184 type Err = RestartPolicyError;
185
186 fn from_str(s: &str) -> Result<Self, Self::Err> {
187 match s {
188 "" => Ok(RestartPolicy::Empty),
189 "no" => Ok(RestartPolicy::No),
190 "always" => Ok(RestartPolicy::Always),
191 "unless-stopped" => Ok(RestartPolicy::UnlessStopped),
192 "on-failure" => Ok(RestartPolicy::OnFailure),
193 _ => Err(RestartPolicyError {
194 value: s.to_string(),
195 }),
196 }
197 }
198}
199
200impl From<RestartPolicy> for RestartPolicyNameEnum {
201 fn from(value: RestartPolicy) -> Self {
202 match value {
203 RestartPolicy::Empty => RestartPolicyNameEnum::EMPTY,
204 RestartPolicy::No => RestartPolicyNameEnum::NO,
205 RestartPolicy::Always => RestartPolicyNameEnum::ALWAYS,
206 RestartPolicy::UnlessStopped => RestartPolicyNameEnum::UNLESS_STOPPED,
207 RestartPolicy::OnFailure => RestartPolicyNameEnum::ON_FAILURE,
208 }
209 }
210}
211
212#[cfg(test)]
213pub(crate) mod tests {
214
215 use std::fmt::Display;
216
217 use astarte_device_sdk::chrono::Utc;
218 use astarte_device_sdk::{AstarteData, DeviceEvent, Value};
219 use pretty_assertions::assert_eq;
220 use uuid::Uuid;
221
222 use super::*;
223
224 pub fn create_container_request_event<S: Display>(
225 id: impl Display,
226 deployment_id: impl Display,
227 image_id: impl Display,
228 image: &str,
229 network_ids: &[S],
230 device_mapping_ids: &[impl Display],
231 ) -> DeviceEvent {
232 let fields = [
233 ("id", AstarteData::String(id.to_string())),
234 (
235 "deploymentId",
236 AstarteData::String(deployment_id.to_string()),
237 ),
238 ("imageId", AstarteData::String(image_id.to_string())),
239 ("volumeIds", AstarteData::StringArray(vec![])),
240 (
241 "deviceMappingIds",
242 AstarteData::StringArray(
243 device_mapping_ids.iter().map(|d| d.to_string()).collect(),
244 ),
245 ),
246 ("image", AstarteData::String(image.to_string())),
247 ("hostname", AstarteData::String("hostname".to_string())),
248 ("restartPolicy", AstarteData::String("no".to_string())),
249 ("env", AstarteData::StringArray(vec!["env".to_string()])),
250 ("binds", AstarteData::StringArray(vec!["binds".to_string()])),
251 ("networkMode", AstarteData::String("bridge".to_string())),
252 (
253 "networkIds",
254 AstarteData::StringArray(network_ids.iter().map(|s| s.to_string()).collect()),
255 ),
256 (
257 "portBindings",
258 AstarteData::StringArray(vec!["80:80".to_string()]),
259 ),
260 (
261 "extraHosts",
262 AstarteData::StringArray(vec!["host.docker.internal:host-gateway".to_string()]),
263 ),
264 (
265 "capAdd",
266 AstarteData::StringArray(vec!["CAP_CHOWN".to_string()]),
267 ),
268 (
269 "capDrop",
270 AstarteData::StringArray(vec!["CAP_KILL".to_string()]),
271 ),
272 ("privileged", AstarteData::Boolean(false)),
273 ("cpuPeriod", AstarteData::LongInteger(1000)),
274 ("cpuQuota", AstarteData::LongInteger(100)),
275 ("cpuRealtimePeriod", AstarteData::LongInteger(1000)),
276 ("cpuRealtimeRuntime", AstarteData::LongInteger(100)),
277 ("memory", AstarteData::LongInteger(4096)),
278 ("memoryReservation", AstarteData::LongInteger(1024)),
279 ("memorySwap", AstarteData::LongInteger(8192)),
280 ("memorySwappiness", AstarteData::Integer(50)),
281 ("volumeDriver", AstarteData::from("local")),
282 (
283 "storageOpt",
284 AstarteData::from(vec!["size=1024k".to_string()]),
285 ),
286 ("readOnlyRootfs", AstarteData::from(true)),
287 (
288 "tmpfs",
289 AstarteData::from(vec!["/run=rw,noexec,nosuid,size=65536k".to_string()]),
290 ),
291 ]
292 .into_iter()
293 .map(|(k, v)| (k.to_string(), v))
294 .collect();
295
296 DeviceEvent {
297 interface: "io.edgehog.devicemanager.apps.CreateContainerRequest".to_string(),
298 path: "/container".to_string(),
299 data: Value::Object {
300 data: fields,
301 timestamp: Utc::now(),
302 },
303 }
304 }
305
306 #[test]
307 fn create_container_request() {
308 let id = ReqUuid(Uuid::new_v4());
309 let deployment_id = ReqUuid(Uuid::new_v4());
310 let image_id = ReqUuid(Uuid::new_v4());
311 let network_ids = VecReqUuid(vec![ReqUuid(Uuid::new_v4())]);
312 let device_mapping_ids = VecReqUuid(vec![ReqUuid(Uuid::new_v4())]);
313 let event = create_container_request_event(
314 id,
315 deployment_id,
316 image_id,
317 "image",
318 &network_ids,
319 &device_mapping_ids,
320 );
321
322 let request = CreateContainer::from_event(event).unwrap();
323
324 let expect = CreateContainer {
325 id,
326 deployment_id,
327 image_id,
328 network_ids,
329 volume_ids: VecReqUuid(vec![]),
330 device_mapping_ids,
331 hostname: "hostname".to_string(),
332 restart_policy: "no".to_string(),
333 env: vec!["env".to_string()],
334 binds: vec!["binds".to_string()],
335 network_mode: "bridge".to_string(),
336 port_bindings: vec!["80:80".to_string()],
337 extra_hosts: vec!["host.docker.internal:host-gateway".to_string()],
338 cap_add: vec!["CAP_CHOWN".to_string()],
339 cap_drop: vec!["CAP_KILL".to_string()],
340 cpu_period: 1000,
341 cpu_quota: 100,
342 cpu_realtime_period: 1000,
343 cpu_realtime_runtime: 100,
344 memory: 4096,
345 memory_reservation: 1024,
346 memory_swap: 8192,
347 memory_swappiness: 50,
348 volume_driver: "local".to_string().into(),
349 storage_opt: vec!["size=1024k".to_string()],
350 read_only_rootfs: true,
351 tmpfs: vec!["/run=rw,noexec,nosuid,size=65536k".to_string()],
352 privileged: false,
353 };
354
355 assert_eq!(request, expect);
356 }
357
358 #[test]
359 fn should_parse_port_binding() {
360 let cases = [
361 (
363 "1.1.1.1:80:90/udp",
364 ParsedBind::new(90, Some("udp"), Some("1.1.1.1"), Some(80)),
365 ),
366 (
367 "1.1.1.1:90/udp",
368 ParsedBind::new(90, Some("udp"), Some("1.1.1.1"), None),
369 ),
370 (
371 "1.1.1.1:90",
372 ParsedBind::new(90, None, Some("1.1.1.1"), None),
373 ),
374 (
376 "80:90/udp",
377 ParsedBind::new(90, Some("udp"), None, Some(80)),
378 ),
379 ("90/udp", ParsedBind::new(90, Some("udp"), None, None)),
380 ("90", ParsedBind::new(90, None, None, None)),
381 ];
382
383 for (case, expected) in cases {
384 let parsed = parse_port_binding(case).unwrap();
385
386 assert_eq!(parsed, expected, "failed to parse {case}");
387 }
388 }
389
390 #[test]
391 fn parse_restart_policy() {
392 let cases = [
393 ("", RestartPolicy::Empty),
394 ("no", RestartPolicy::No),
395 ("unless-stopped", RestartPolicy::UnlessStopped),
396 ("on-failure", RestartPolicy::OnFailure),
397 ("on-failure", RestartPolicy::OnFailure),
398 ];
399
400 for (case, exp) in cases {
401 let policy = RestartPolicy::from_str(case).unwrap();
402
403 assert_eq!(policy, exp);
404 }
405
406 let err = RestartPolicy::from_str("bar").unwrap_err();
407 assert_eq!(
408 err,
409 RestartPolicyError {
410 value: "bar".to_string()
411 }
412 );
413
414 let err = RestartPolicy::from_str("NO").unwrap_err();
415 assert_eq!(
416 err,
417 RestartPolicyError {
418 value: "NO".to_string()
419 }
420 );
421
422 let err = RestartPolicy::from_str("on_failure").unwrap_err();
423 assert_eq!(
424 err,
425 RestartPolicyError {
426 value: "on_failure".to_string()
427 }
428 );
429 }
430}