Skip to main content

edgehog_device_runtime_containers/store/
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//! Persistent stores of the request issued by Astarte and resources created.
20
21use std::ops::Not;
22
23use edgehog_store::db::{self, HandleError};
24
25use crate::requests::{container::RestartPolicyError, BindingError};
26
27mod container;
28mod deployment;
29mod device_mapping;
30mod image;
31mod network;
32mod volume;
33
34type Result<T> = std::result::Result<T, StoreError>;
35
36/// Error returned by the [`StateStore`].
37#[derive(Debug, thiserror::Error, displaydoc::Display)]
38pub enum StoreError {
39    /// couldn't parse {ctx} key value {value}
40    ParseKeyValue {
41        /// Key that couldn't be parsed
42        ctx: &'static str,
43        /// Value that couldn't be parsed
44        value: String,
45    },
46    /// couldn't parse container port bindings
47    PortBinding(#[from] BindingError),
48    /// couldn't parse the container restart policy
49    RestartPolicy(#[from] RestartPolicyError),
50    /// database operation failed
51    Handle(#[from] HandleError),
52    /// conversion failed, {ctx}
53    Conversion {
54        /// Context of the error
55        ctx: String,
56    },
57}
58
59/// Handle to persist the state.
60///
61/// It's a wrapper around the SQLITE database.
62#[derive(Debug, Clone)]
63pub struct StateStore {
64    handle: db::Handle,
65}
66
67impl StateStore {
68    /// Creates a new state store
69    pub fn new(handle: db::Handle) -> Self {
70        Self { handle }
71    }
72}
73
74#[allow(dead_code)]
75fn split_key_value(value: &str) -> Option<(&str, Option<&str>)> {
76    value.split_once('=').and_then(|(k, v)| {
77        if k.is_empty() {
78            return None;
79        }
80
81        let v = v.is_empty().not().then_some(v);
82
83        Some((k, v))
84    })
85}
86
87#[cfg(test)]
88mod tests {
89    use pretty_assertions::assert_eq;
90    use tempfile::TempDir;
91    use uuid::Uuid;
92
93    use crate::requests::{
94        container::CreateContainer, deployment::CreateDeployment, image::CreateImage,
95        network::CreateNetwork, volume::CreateVolume, ReqUuid, VecReqUuid,
96    };
97
98    use super::*;
99
100    #[test]
101    fn should_parse_key_value() {
102        let cases = [
103            ("device=tmpfs", ("device", Some("tmpfs"))),
104            ("o=size=100m,uid=1000", ("o", Some("size=100m,uid=1000"))),
105            ("type=tmpfs", ("type", Some("tmpfs"))),
106        ];
107
108        for (case, exp) in cases {
109            let res = split_key_value(case).unwrap();
110
111            assert_eq!(res, exp);
112        }
113    }
114
115    #[tokio::test]
116    async fn should_create_missing() {
117        let tmp = TempDir::with_prefix("create_full_deployment").unwrap();
118        let db_file = tmp.path().join("state.db");
119        let db_file = db_file.to_str().unwrap();
120
121        let handle = db::Handle::open(db_file).await.unwrap();
122        let store = StateStore::new(handle);
123
124        let image_id = Uuid::new_v4();
125        let volume_id = ReqUuid(Uuid::new_v4());
126        let network_id = ReqUuid(Uuid::new_v4());
127        let device_mapping_id = ReqUuid(Uuid::new_v4());
128        let container_id = ReqUuid(Uuid::new_v4());
129        let deployment_id = ReqUuid(Uuid::new_v4());
130
131        let deployment = CreateDeployment {
132            id: deployment_id,
133            containers: VecReqUuid(vec![container_id]),
134        };
135        store.create_deployment(deployment).await.unwrap();
136
137        let container = CreateContainer {
138            id: container_id,
139            deployment_id: ReqUuid(image_id),
140            image_id: ReqUuid(image_id),
141            network_ids: VecReqUuid(vec![network_id]),
142            volume_ids: VecReqUuid(vec![volume_id]),
143            device_mapping_ids: VecReqUuid(vec![device_mapping_id]),
144            hostname: "database".to_string(),
145            restart_policy: "unless-stopped".to_string(),
146            env: ["POSTGRES_USER=user", "POSTGRES_PASSWORD=password"]
147                .map(str::to_string)
148                .to_vec(),
149            binds: vec!["/var/lib/postgres".to_string()],
150            network_mode: "bridge".to_string(),
151            port_bindings: vec!["5432:5432".to_string()],
152            extra_hosts: vec!["host.docker.internal:host-gateway".to_string()],
153            cap_add: vec!["CAP_CHOWN".to_string()],
154            cap_drop: vec!["CAP_KILL".to_string()],
155            cpu_period: 1000,
156            cpu_quota: 100,
157            cpu_realtime_period: 1000,
158            cpu_realtime_runtime: 100,
159            memory: 4096,
160            memory_reservation: 1024,
161            memory_swap: 8192,
162            memory_swappiness: 50,
163            volume_driver: "local".to_string().into(),
164            storage_opt: vec!["size=1024k".to_string()],
165            read_only_rootfs: true,
166            tmpfs: vec!["/run=rw,noexec,nosuid,size=65536k".to_string()],
167            privileged: false,
168        };
169        store.create_container(Box::new(container)).await.unwrap();
170
171        let network = CreateNetwork {
172            id: network_id,
173            deployment_id,
174            driver: "bridge".to_string(),
175            internal: true,
176            enable_ipv6: false,
177            options: vec!["isolate=true".to_string()],
178        };
179        store.create_network(network).await.unwrap();
180
181        let volume = CreateVolume {
182            id: volume_id,
183            deployment_id,
184            driver: "local".to_string(),
185            options: ["device=tmpfs", "o=size=100m,uid=1000", "type=tmpfs"]
186                .map(str::to_string)
187                .to_vec(),
188        };
189        store.create_volume(volume).await.unwrap();
190
191        let image = CreateImage {
192            id: ReqUuid(image_id),
193            deployment_id,
194            reference: "postgres:15".to_string(),
195            registry_auth: String::new(),
196        };
197        store.create_image(image).await.unwrap();
198    }
199}