Skip to main content

edgehog_device_runtime_containers/store/
deployment.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::dsl::exists;
20use diesel::{
21    delete, insert_or_ignore_into, select, update, CombineDsl, ExpressionMethods,
22    NullableExpressionMethods, QueryDsl, RunQueryDsl, SelectableHelper, SqliteConnection,
23};
24use edgehog_store::conversions::SqlUuid;
25use edgehog_store::db::HandleError;
26use edgehog_store::models::containers::container::{
27    Container, ContainerDeviceMapping, ContainerNetwork, ContainerVolume,
28};
29use edgehog_store::models::containers::deployment::{
30    Deployment, DeploymentContainer, DeploymentMissingContainer, DeploymentStatus,
31};
32use edgehog_store::models::QueryModel;
33use edgehog_store::schema::containers::{
34    container_device_mappings, container_missing_images, container_missing_networks,
35    container_missing_volumes, container_networks, container_volumes, containers,
36    deployment_containers, deployment_missing_containers, deployments,
37};
38use itertools::Itertools;
39use tracing::{debug, instrument};
40use uuid::Uuid;
41
42use crate::requests::deployment::{CreateDeployment, DeploymentUpdate};
43use crate::resource::deployment::Deployment as DeploymentResource;
44
45use super::{Result, StateStore};
46
47impl StateStore {
48    /// Stores a received deployment
49    #[instrument(skip_all, fields(%deployment.id))]
50    pub(crate) async fn create_deployment(&self, deployment: CreateDeployment) -> Result<()> {
51        let containers = deployment.containers.iter().map(SqlUuid::new).collect_vec();
52        let deployment = Deployment::from(deployment);
53
54        self.handle
55            .for_write(move |writer| {
56                insert_or_ignore_into(deployments::table)
57                    .values(&deployment)
58                    .execute(writer)?;
59
60                for container_id in containers {
61                    let exists: bool = Container::exists(&container_id).get_result(writer)?;
62
63                    if !exists {
64                        insert_or_ignore_into(deployment_missing_containers::table)
65                            .values(DeploymentMissingContainer {
66                                deployment_id: deployment.id,
67                                container_id,
68                            })
69                            .execute(writer)?;
70
71                        continue;
72                    }
73
74                    insert_or_ignore_into(deployment_containers::table)
75                        .values(DeploymentContainer {
76                            deployment_id: deployment.id,
77                            container_id,
78                        })
79                        .execute(writer)?;
80                }
81
82                Ok(())
83            })
84            .await?;
85
86        Ok(())
87    }
88
89    /// Updates the status of a deployment
90    #[instrument(skip(self))]
91    pub(crate) async fn update_deployment_status(
92        &self,
93        id: Uuid,
94        status: DeploymentStatus,
95    ) -> Result<()> {
96        self.handle
97            .for_write(move |writer| {
98                let updated = update(Deployment::find_id(&SqlUuid::new(id)))
99                    .set(deployments::status.eq(status))
100                    .execute(writer)?;
101
102                HandleError::check_modified(updated, 1)?;
103
104                Ok(())
105            })
106            .await?;
107
108        Ok(())
109    }
110
111    #[instrument(skip(self))]
112    pub(crate) async fn delete_deployment(&self, id: Uuid) -> Result<()> {
113        self.handle
114            .for_write(move |writer| {
115                let updated = delete(Deployment::find_id(&SqlUuid::new(id))).execute(writer)?;
116
117                HandleError::check_modified(updated, 1)?;
118
119                Ok(())
120            })
121            .await?;
122
123        Ok(())
124    }
125
126    /// Updates the status of two deployment atomically
127    #[instrument(skip(self))]
128    pub(crate) async fn deployment_update(&self, from: Uuid, to: Uuid) -> Result<()> {
129        self.handle
130            .for_write(move |writer| {
131                let status: DeploymentStatus = Deployment::find_id(&SqlUuid::new(from))
132                    .select(deployments::status)
133                    .first(writer)?;
134
135                // Do nothing if the status is not started
136                match status {
137                    DeploymentStatus::Started => {
138                        let updated = update(Deployment::find_id(&SqlUuid::new(from)))
139                            .set(deployments::status.eq(DeploymentStatus::Stopped))
140                            .execute(writer)?;
141
142                        HandleError::check_modified(updated, 1)?;
143
144                        debug!("deployment to update set to stopped")
145                    }
146                    DeploymentStatus::Received
147                    | DeploymentStatus::Stopped
148                    | DeploymentStatus::Deleted => {
149                        debug!("deployment to update is in state {status}, not setting to stopped")
150                    }
151                }
152
153                let updated = update(Deployment::find_id(&SqlUuid::new(to)))
154                    .set(deployments::status.eq(DeploymentStatus::Started))
155                    .execute(writer)?;
156
157                HandleError::check_modified(updated, 1)?;
158
159                Ok(())
160            })
161            .await?;
162
163        Ok(())
164    }
165
166    #[instrument(skip(self))]
167    pub(crate) async fn load_deployments_in(
168        &self,
169        status: DeploymentStatus,
170    ) -> Result<Vec<SqlUuid>> {
171        let deployment = self
172            .handle
173            .for_read(move |reader| {
174                let deployments = deployments::table
175                    .filter(deployments::status.eq(status))
176                    .select(deployments::id)
177                    .load::<SqlUuid>(reader)?;
178
179                Ok(deployments)
180            })
181            .await?;
182
183        Ok(deployment)
184    }
185
186    /// Fetches the containers for a deployment
187    #[instrument(skip(self))]
188    pub(crate) async fn load_deployment_containers(
189        &self,
190        id: Uuid,
191    ) -> Result<Option<Vec<SqlUuid>>> {
192        let containers = self
193            .handle
194            .for_read(move |reader| {
195                let id = SqlUuid::new(id);
196                if !Deployment::exists(&id).get_result(reader)? {
197                    return Ok(None);
198                }
199
200                let containers = deployment_containers::table
201                    .select(deployment_containers::container_id)
202                    .filter(deployment_containers::deployment_id.eq(id))
203                    .load::<SqlUuid>(reader)?;
204
205                Ok(Some(containers))
206            })
207            .await?;
208
209        Ok(containers)
210    }
211
212    pub(crate) async fn find_complete_deployment(
213        &self,
214        id: Uuid,
215    ) -> Result<Option<DeploymentResource>> {
216        let deployment = self
217            .handle
218            .for_read(move |reader| {
219                let id = SqlUuid::new(id);
220
221                if !Deployment::exists(&id).get_result(reader)? {
222                    return Ok(None);
223                }
224
225                if !is_deployment_complete(reader, &id)? {
226                    return Ok(None);
227                }
228
229                let rows = Deployment::join_resources()
230                    .filter(deployment_containers::deployment_id.eq(id))
231                    .select((
232                        deployment_containers::container_id,
233                        containers::image_id.assume_not_null(),
234                        Option::<ContainerNetwork>::as_select(),
235                        Option::<ContainerVolume>::as_select(),
236                        Option::<ContainerDeviceMapping>::as_select(),
237                    ))
238                    .load::<(
239                        SqlUuid,
240                        SqlUuid,
241                        Option<ContainerNetwork>,
242                        Option<ContainerVolume>,
243                        Option<ContainerDeviceMapping>,
244                    )>(reader)?;
245
246                Ok(Some(DeploymentResource::from(rows)))
247            })
248            .await?;
249
250        Ok(deployment)
251    }
252
253    pub(crate) async fn find_deployment_for_delete(
254        &self,
255        id: Uuid,
256    ) -> Result<Option<DeploymentResource>> {
257        let deployment = self
258            .handle
259            .for_read(move |reader| {
260                let id = SqlUuid::new(id);
261
262                if !Deployment::exists(&id).get_result(reader)? {
263                    return Ok(None);
264                }
265
266                // Delete only the resources present in this deployment
267                let containers = Deployment::join_resources()
268                    .filter(deployment_containers::deployment_id.eq(id))
269                    .select(deployment_containers::container_id)
270                    .except(
271                        Deployment::join_resources()
272                            .filter(deployment_containers::deployment_id.ne(id))
273                            .select(deployment_containers::container_id),
274                    )
275                    .load::<SqlUuid>(reader)?
276                    .into_iter()
277                    .map(Uuid::from)
278                    .collect();
279
280                let images = Deployment::join_resources()
281                    .filter(deployment_containers::deployment_id.eq(id))
282                    .select(containers::image_id.assume_not_null())
283                    .except(
284                        Deployment::join_resources()
285                            .filter(deployment_containers::deployment_id.ne(id))
286                            .select(containers::image_id.assume_not_null()),
287                    )
288                    .load::<SqlUuid>(reader)?
289                    .into_iter()
290                    .map(Uuid::from)
291                    .collect();
292
293                let volumes = Deployment::join_resources()
294                    .filter(deployment_containers::deployment_id.eq(id))
295                    .select(container_volumes::volume_id.nullable())
296                    .except(
297                        Deployment::join_resources()
298                            .filter(deployment_containers::deployment_id.ne(id))
299                            .select(container_volumes::volume_id.nullable()),
300                    )
301                    .load::<Option<SqlUuid>>(reader)?
302                    .into_iter()
303                    .filter_map(|container_volume| container_volume.map(Uuid::from))
304                    .collect();
305
306                let networks = Deployment::join_resources()
307                    .filter(deployment_containers::deployment_id.eq(id))
308                    .select(container_networks::network_id.nullable())
309                    .except(
310                        Deployment::join_resources()
311                            .filter(deployment_containers::deployment_id.ne(id))
312                            .select(container_networks::network_id.nullable()),
313                    )
314                    .load::<Option<SqlUuid>>(reader)?
315                    .into_iter()
316                    .filter_map(|container_network| container_network.map(Uuid::from))
317                    .collect();
318
319                let device_mapping = Deployment::join_resources()
320                    .filter(deployment_containers::deployment_id.eq(id))
321                    .select(container_device_mappings::device_mapping_id.nullable())
322                    .except(
323                        Deployment::join_resources()
324                            .filter(deployment_containers::deployment_id.ne(id))
325                            .select(container_device_mappings::device_mapping_id.nullable()),
326                    )
327                    .load::<Option<SqlUuid>>(reader)?
328                    .into_iter()
329                    .filter_map(|container_device_mapping| container_device_mapping.map(Uuid::from))
330                    .collect();
331
332                Ok(Some(DeploymentResource {
333                    containers,
334                    images,
335                    volumes,
336                    networks,
337                    device_mapping,
338                }))
339            })
340            .await?;
341
342        Ok(deployment)
343    }
344
345    /// Fetches the containers for a deployment to be stopped for an update
346    #[instrument(skip(self))]
347    pub(crate) async fn load_deployment_containers_update_from(
348        &self,
349        DeploymentUpdate { from, to }: DeploymentUpdate,
350    ) -> Result<Option<Vec<SqlUuid>>> {
351        let containers = self
352            .handle
353            .for_read(move |reader| {
354                let from = SqlUuid::new(from);
355                if !Deployment::exists(&from).get_result(reader)? {
356                    return Ok(None);
357                }
358
359                let to = SqlUuid::new(to);
360
361                let containers = deployment_containers::table
362                    .select(deployment_containers::container_id)
363                    .filter(deployment_containers::deployment_id.eq(from))
364                    // Exclude container in the update
365                    .except(
366                        deployment_containers::table
367                            .select(deployment_containers::container_id)
368                            .filter(deployment_containers::deployment_id.eq(to)),
369                    )
370                    .load::<SqlUuid>(reader)?;
371
372                Ok(Some(containers))
373            })
374            .await?;
375
376        Ok(containers)
377    }
378}
379
380/// Check that a deployment with the given id exists, and there are no missing rows
381/// for the various resources
382fn is_deployment_complete(
383    reader: &mut SqliteConnection,
384    id: &SqlUuid,
385) -> std::result::Result<bool, HandleError> {
386    select(exists(
387        deployments::table
388            .left_join(deployment_missing_containers::table)
389            .inner_join(
390                deployment_containers::table.inner_join(
391                    containers::table
392                        .left_join(container_missing_images::table)
393                        .left_join(container_missing_networks::table)
394                        .left_join(container_missing_volumes::table),
395                ),
396            )
397            .filter(deployments::id.eq(id))
398            .filter(deployment_missing_containers::deployment_id.is_null())
399            .filter(container_missing_images::container_id.is_null())
400            .filter(container_missing_networks::container_id.is_null())
401            .filter(container_missing_volumes::container_id.is_null()),
402    ))
403    .first::<bool>(reader)
404    .map_err(HandleError::Query)
405}
406
407impl From<CreateDeployment> for Deployment {
408    fn from(CreateDeployment { id, containers: _ }: CreateDeployment) -> Self {
409        Self {
410            id: SqlUuid::new(id),
411            status: DeploymentStatus::default(),
412        }
413    }
414}
415
416#[cfg(test)]
417mod tests {
418    use std::collections::HashSet;
419
420    use diesel::OptionalExtension;
421    use edgehog_store::db;
422    use pretty_assertions::assert_eq;
423    use tempfile::TempDir;
424
425    use crate::requests::device_mapping::CreateDeviceMapping;
426    use crate::requests::OptString;
427    use crate::requests::{
428        container::CreateContainer, image::CreateImage, network::CreateNetwork,
429        volume::CreateVolume, ReqUuid, VecReqUuid,
430    };
431
432    use super::*;
433
434    pub(crate) async fn find_deployment(store: &StateStore, id: Uuid) -> Option<Deployment> {
435        store
436            .handle
437            .for_read(move |reader| {
438                Deployment::find_id(&SqlUuid::new(id))
439                    .first::<Deployment>(reader)
440                    .optional()
441                    .map_err(HandleError::Query)
442            })
443            .await
444            .unwrap()
445    }
446
447    #[tokio::test]
448    async fn should_create() {
449        let tmp = TempDir::with_prefix("create_full_deployment").unwrap();
450        let db_file = tmp.path().join("state.db");
451        let db_file = db_file.to_str().unwrap();
452
453        let handle = db::Handle::open(db_file).await.unwrap();
454        let store = StateStore::new(handle);
455
456        let deployment_id = Uuid::new_v4();
457
458        let image_id = Uuid::new_v4();
459        let image = CreateImage {
460            id: ReqUuid(image_id),
461            deployment_id: ReqUuid(deployment_id),
462            reference: "postgres:15".to_string(),
463            registry_auth: String::new(),
464        };
465        store.create_image(image).await.unwrap();
466
467        let volume_id = ReqUuid(Uuid::new_v4());
468        let volume = CreateVolume {
469            id: volume_id,
470            deployment_id: ReqUuid(deployment_id),
471            driver: "local".to_string(),
472            options: ["device=tmpfs", "o=size=100m,uid=1000", "type=tmpfs"]
473                .map(str::to_string)
474                .to_vec(),
475        };
476        store.create_volume(volume).await.unwrap();
477
478        let network_id = ReqUuid(Uuid::new_v4());
479        let network = CreateNetwork {
480            id: network_id,
481            deployment_id: ReqUuid(deployment_id),
482            driver: "bridge".to_string(),
483            internal: true,
484            enable_ipv6: false,
485            options: vec!["isolate=true".to_string()],
486        };
487        store.create_network(network).await.unwrap();
488
489        let device_mapping_id = ReqUuid(Uuid::new_v4());
490        let device_mapping = CreateDeviceMapping {
491            id: device_mapping_id,
492            deployment_id: ReqUuid(deployment_id),
493            path_on_host: "/dev/tty12".to_string(),
494            path_in_container: "dev/tty12".to_string(),
495            c_group_permissions: OptString::from("msv".to_string()),
496        };
497        store.create_device_mapping(device_mapping).await.unwrap();
498
499        let container_id = Uuid::new_v4();
500        let container = CreateContainer {
501            id: ReqUuid(container_id),
502            deployment_id: ReqUuid(deployment_id),
503            image_id: ReqUuid(image_id),
504            network_ids: VecReqUuid(vec![network_id]),
505            volume_ids: VecReqUuid(vec![volume_id]),
506            device_mapping_ids: VecReqUuid(vec![device_mapping_id]),
507            hostname: "database".to_string(),
508            restart_policy: "unless-stopped".to_string(),
509            env: ["POSTGRES_USER=user", "POSTGRES_PASSWORD=password"]
510                .map(str::to_string)
511                .to_vec(),
512            binds: vec!["/var/lib/postgres".to_string()],
513            network_mode: "bridge".to_string(),
514            port_bindings: vec!["5432:5432".to_string()],
515            extra_hosts: vec!["host.docker.internal:host-gateway".to_string()],
516            cap_add: vec!["CAP_CHOWN".to_string()],
517            cap_drop: vec!["CAP_KILL".to_string()],
518            cpu_period: 1000,
519            cpu_quota: 100,
520            cpu_realtime_period: 1000,
521            cpu_realtime_runtime: 100,
522            memory: 4096,
523            memory_reservation: 1024,
524            memory_swap: 8192,
525            memory_swappiness: 50,
526            volume_driver: "local".to_string().into(),
527            storage_opt: vec!["size=1024k".to_string()],
528            read_only_rootfs: true,
529            tmpfs: vec!["/run=rw,noexec,nosuid,size=65536k".to_string()],
530            privileged: false,
531        };
532        store.create_container(Box::new(container)).await.unwrap();
533
534        let deployment_id = Uuid::new_v4();
535        let deployment = CreateDeployment {
536            id: ReqUuid(deployment_id),
537            containers: VecReqUuid(vec![ReqUuid(container_id)]),
538        };
539        store.create_deployment(deployment).await.unwrap();
540
541        let deployment = find_deployment(&store, deployment_id).await.unwrap();
542        let exp = Deployment {
543            id: SqlUuid::new(deployment_id),
544            status: DeploymentStatus::Received,
545        };
546        assert_eq!(deployment, exp);
547
548        let containers = store
549            .load_deployment_containers(deployment_id)
550            .await
551            .unwrap()
552            .unwrap();
553        let exp = vec![SqlUuid::new(container_id)];
554        assert_eq!(containers, exp);
555    }
556
557    #[tokio::test]
558    async fn update_status() {
559        let tmp = TempDir::with_prefix("create_full_deployment").unwrap();
560        let db_file = tmp.path().join("state.db");
561        let db_file = db_file.to_str().unwrap();
562
563        let handle = db::Handle::open(db_file).await.unwrap();
564        let store = StateStore::new(handle);
565
566        let container_id = Uuid::new_v4();
567        let deployment_id = Uuid::new_v4();
568        let deployment = CreateDeployment {
569            id: ReqUuid(deployment_id),
570            containers: VecReqUuid(vec![ReqUuid(container_id)]),
571        };
572        store.create_deployment(deployment).await.unwrap();
573
574        store
575            .update_deployment_status(deployment_id, DeploymentStatus::Stopped)
576            .await
577            .unwrap();
578
579        let deployment = find_deployment(&store, deployment_id).await.unwrap();
580        let exp = Deployment {
581            id: SqlUuid::new(deployment_id),
582            status: DeploymentStatus::Stopped,
583        };
584        assert_eq!(deployment, exp);
585    }
586
587    #[tokio::test]
588    async fn find_complete_deployment() {
589        let tmp = TempDir::with_prefix("create_full_deployment").unwrap();
590        let db_file = tmp.path().join("state.db");
591        let db_file = db_file.to_str().unwrap();
592
593        let handle = db::Handle::open(db_file).await.unwrap();
594        let store = StateStore::new(handle);
595
596        let deployment_id = Uuid::new_v4();
597
598        let image_id = Uuid::new_v4();
599        let image = CreateImage {
600            id: ReqUuid(image_id),
601            deployment_id: ReqUuid(deployment_id),
602            reference: "postgres:15".to_string(),
603            registry_auth: String::new(),
604        };
605        store.create_image(image).await.unwrap();
606
607        let volume_id = ReqUuid(Uuid::new_v4());
608        let volume = CreateVolume {
609            id: volume_id,
610            deployment_id: ReqUuid(deployment_id),
611            driver: "local".to_string(),
612            options: ["device=tmpfs", "o=size=100m,uid=1000", "type=tmpfs"]
613                .map(str::to_string)
614                .to_vec(),
615        };
616        store.create_volume(volume).await.unwrap();
617
618        let network_id = ReqUuid(Uuid::new_v4());
619        let network = CreateNetwork {
620            id: network_id,
621            deployment_id: ReqUuid(deployment_id),
622            driver: "bridge".to_string(),
623            internal: true,
624            enable_ipv6: false,
625            options: vec!["isolate=true".to_string()],
626        };
627        store.create_network(network).await.unwrap();
628
629        let device_mapping_id = ReqUuid(Uuid::new_v4());
630        let device_mapping = CreateDeviceMapping {
631            id: device_mapping_id,
632            deployment_id: ReqUuid(deployment_id),
633            path_on_host: "/dev/tty12".to_string(),
634            path_in_container: "dev/tty12".to_string(),
635            c_group_permissions: OptString::from("msv".to_string()),
636        };
637        store.create_device_mapping(device_mapping).await.unwrap();
638
639        let container_id = Uuid::new_v4();
640        let container = CreateContainer {
641            id: ReqUuid(container_id),
642            deployment_id: ReqUuid(deployment_id),
643            image_id: ReqUuid(image_id),
644            network_ids: VecReqUuid(vec![network_id]),
645            volume_ids: VecReqUuid(vec![volume_id]),
646            device_mapping_ids: VecReqUuid(vec![device_mapping_id]),
647            hostname: "database".to_string(),
648            restart_policy: "unless-stopped".to_string(),
649            env: ["POSTGRES_USER=user", "POSTGRES_PASSWORD=password"]
650                .map(str::to_string)
651                .to_vec(),
652            binds: vec!["/var/lib/postgres".to_string()],
653            network_mode: "bridge".to_string(),
654            port_bindings: vec!["5432:5432".to_string()],
655            extra_hosts: vec!["host.docker.internal:host-gateway".to_string()],
656            cap_add: vec!["CAP_CHOWN".to_string()],
657            cap_drop: vec!["CAP_KILL".to_string()],
658            cpu_period: 1000,
659            cpu_quota: 100,
660            cpu_realtime_period: 1000,
661            cpu_realtime_runtime: 100,
662            memory: 4096,
663            memory_reservation: 1024,
664            memory_swap: 8192,
665            memory_swappiness: 50,
666            volume_driver: "local".to_string().into(),
667            storage_opt: vec!["size=1024k".to_string()],
668            read_only_rootfs: true,
669            tmpfs: vec!["/run=rw,noexec,nosuid,size=65536k".to_string()],
670            privileged: false,
671        };
672        store.create_container(Box::new(container)).await.unwrap();
673
674        let deployment_id = Uuid::new_v4();
675        let deployment = CreateDeployment {
676            id: ReqUuid(deployment_id),
677            containers: VecReqUuid(vec![ReqUuid(container_id)]),
678        };
679        store.create_deployment(deployment).await.unwrap();
680
681        let deployment = store
682            .find_complete_deployment(deployment_id)
683            .await
684            .unwrap()
685            .unwrap();
686        let exp = DeploymentResource {
687            containers: HashSet::from_iter([container_id]),
688            images: HashSet::from_iter([image_id]),
689            volumes: HashSet::from_iter([volume_id.0]),
690            networks: HashSet::from_iter([network_id.0]),
691            device_mapping: HashSet::from_iter([device_mapping_id.0]),
692        };
693
694        assert_eq!(deployment, exp);
695    }
696
697    #[tokio::test]
698    async fn shared_resources_delete() {
699        let tmp = TempDir::with_prefix("create_full_deployment").unwrap();
700        let db_file = tmp.path().join("state.db");
701        let db_file = db_file.to_str().unwrap();
702
703        let handle = db::Handle::open(db_file).await.unwrap();
704        let store = StateStore::new(handle);
705
706        let deployment_id_1 = Uuid::new_v4();
707
708        let image_id = Uuid::new_v4();
709        let image = CreateImage {
710            id: ReqUuid(image_id),
711            deployment_id: ReqUuid(deployment_id_1),
712            reference: "postgres:15".to_string(),
713            registry_auth: String::new(),
714        };
715        store.create_image(image).await.unwrap();
716
717        let volume_id = ReqUuid(Uuid::new_v4());
718        let volume = CreateVolume {
719            id: volume_id,
720            deployment_id: ReqUuid(deployment_id_1),
721            driver: "local".to_string(),
722            options: ["device=tmpfs", "o=size=100m,uid=1000", "type=tmpfs"]
723                .map(str::to_string)
724                .to_vec(),
725        };
726        store.create_volume(volume).await.unwrap();
727
728        let network_id = ReqUuid(Uuid::new_v4());
729        let network = CreateNetwork {
730            id: network_id,
731            deployment_id: ReqUuid(deployment_id_1),
732            driver: "bridge".to_string(),
733            internal: true,
734            enable_ipv6: false,
735            options: vec!["isolate=true".to_string()],
736        };
737        store.create_network(network).await.unwrap();
738
739        let device_mapping_id = ReqUuid(Uuid::new_v4());
740        let device_mapping = CreateDeviceMapping {
741            id: device_mapping_id,
742            deployment_id: ReqUuid(deployment_id_1),
743            path_on_host: "/dev/tty12".to_string(),
744            path_in_container: "dev/tty12".to_string(),
745            c_group_permissions: OptString::from("msv".to_string()),
746        };
747        store.create_device_mapping(device_mapping).await.unwrap();
748
749        let container_id_1 = Uuid::new_v4();
750        let container_1 = CreateContainer {
751            id: ReqUuid(container_id_1),
752            deployment_id: ReqUuid(deployment_id_1),
753            image_id: ReqUuid(image_id),
754            network_ids: VecReqUuid(vec![network_id]),
755            volume_ids: VecReqUuid(vec![volume_id]),
756            device_mapping_ids: VecReqUuid(vec![device_mapping_id]),
757            hostname: "database".to_string(),
758            restart_policy: "unless-stopped".to_string(),
759            env: ["POSTGRES_USER=user", "POSTGRES_PASSWORD=password"]
760                .map(str::to_string)
761                .to_vec(),
762            binds: vec!["/var/lib/postgres".to_string()],
763            network_mode: "bridge".to_string(),
764            port_bindings: vec!["5432:5432".to_string()],
765            extra_hosts: vec!["host.docker.internal:host-gateway".to_string()],
766            cap_add: vec!["CAP_CHOWN".to_string()],
767            cap_drop: vec!["CAP_KILL".to_string()],
768            cpu_period: 1000,
769            cpu_quota: 100,
770            cpu_realtime_period: 1000,
771            cpu_realtime_runtime: 100,
772            memory: 4096,
773            memory_reservation: 1024,
774            memory_swap: 8192,
775            memory_swappiness: 50,
776            volume_driver: "local".to_string().into(),
777            storage_opt: vec!["size=1024k".to_string()],
778            read_only_rootfs: true,
779            tmpfs: vec!["/run=rw,noexec,nosuid,size=65536k".to_string()],
780            privileged: false,
781        };
782        store.create_container(Box::new(container_1)).await.unwrap();
783
784        let deployment_1 = CreateDeployment {
785            id: ReqUuid(deployment_id_1),
786            containers: VecReqUuid(vec![ReqUuid(container_id_1)]),
787        };
788        store.create_deployment(deployment_1).await.unwrap();
789
790        let deployment_id_2 = Uuid::new_v4();
791        let container_id_2 = Uuid::new_v4();
792        let container_2 = CreateContainer {
793            id: ReqUuid(container_id_2),
794            deployment_id: ReqUuid(deployment_id_2),
795            image_id: ReqUuid(image_id),
796            network_ids: VecReqUuid(vec![network_id]),
797            volume_ids: VecReqUuid(vec![volume_id]),
798            device_mapping_ids: VecReqUuid(vec![device_mapping_id]),
799            hostname: "database".to_string(),
800            restart_policy: "unless-stopped".to_string(),
801            env: ["POSTGRES_USER=user", "POSTGRES_PASSWORD=password"]
802                .map(str::to_string)
803                .to_vec(),
804            binds: vec!["/var/lib/postgres".to_string()],
805            network_mode: "bridge".to_string(),
806            port_bindings: vec!["5432:5432".to_string()],
807            extra_hosts: vec!["host.docker.internal:host-gateway".to_string()],
808            cap_add: vec!["CAP_CHOWN".to_string()],
809            cap_drop: vec!["CAP_KILL".to_string()],
810            cpu_period: 1000,
811            cpu_quota: 100,
812            cpu_realtime_period: 1000,
813            cpu_realtime_runtime: 100,
814            memory: 4096,
815            memory_reservation: 1024,
816            memory_swap: 8192,
817            memory_swappiness: 50,
818            volume_driver: "local".to_string().into(),
819            storage_opt: vec!["size=1024k".to_string()],
820            read_only_rootfs: true,
821            tmpfs: vec!["/run=rw,noexec,nosuid,size=65536k".to_string()],
822            privileged: false,
823        };
824        store.create_container(Box::new(container_2)).await.unwrap();
825
826        let deployment_2 = CreateDeployment {
827            id: ReqUuid(deployment_id_2),
828            containers: VecReqUuid(vec![ReqUuid(container_id_2)]),
829        };
830        store.create_deployment(deployment_2).await.unwrap();
831
832        let res = store
833            .find_deployment_for_delete(deployment_id_1)
834            .await
835            .unwrap()
836            .unwrap();
837
838        let exp = DeploymentResource {
839            containers: HashSet::from_iter([container_id_1]),
840            images: HashSet::new(),
841            volumes: HashSet::new(),
842            networks: HashSet::new(),
843            device_mapping: HashSet::new(),
844        };
845
846        assert_eq!(res, exp);
847    }
848}