edgehog_device_runtime_containers/store/
image.rs1use diesel::{delete, insert_or_ignore_into, ExpressionMethods, OptionalExtension, RunQueryDsl};
20use diesel::{update, QueryDsl};
21use edgehog_store::conversions::SqlUuid;
22use edgehog_store::db::HandleError;
23use edgehog_store::models::containers::image::ImageStatus;
24use edgehog_store::models::QueryModel;
25use edgehog_store::{
26 models::containers::{container::ContainerMissingImage, image::Image},
27 schema::containers::{container_missing_images, containers, images},
28};
29use tracing::instrument;
30use uuid::Uuid;
31
32use crate::docker::image::Image as ContainerImage;
33use crate::requests::image::CreateImage;
34use crate::resource::image::ImageResource;
35
36use super::{Result, StateStore};
37
38impl StateStore {
39 #[instrument(skip_all, fields(%image.id))]
41 pub(crate) async fn create_image(&self, image: CreateImage) -> Result<()> {
42 let image = Image::from(image);
43
44 self.handle
45 .for_write(move |writer| {
46 insert_or_ignore_into(images::table)
47 .values(&image)
48 .execute(writer)?;
49
50 update(containers::table)
51 .set(containers::image_id.eq(image.id))
52 .filter(
53 containers::id.eq_any(
54 ContainerMissingImage::find_by_image(&image.id)
55 .select(container_missing_images::container_id),
56 ),
57 )
58 .execute(writer)?;
59
60 delete(ContainerMissingImage::find_by_image(&image.id)).execute(writer)?;
61
62 Ok(())
63 })
64 .await?;
65
66 Ok(())
67 }
68
69 #[instrument(skip(self))]
71 pub(crate) async fn update_image_status(&self, id: Uuid, status: ImageStatus) -> Result<()> {
72 self.handle
73 .for_write(move |writer| {
74 let updated = update(Image::find_id(&SqlUuid::new(id)))
75 .set(images::status.eq(status))
76 .execute(writer)?;
77
78 HandleError::check_modified(updated, 1)?;
79
80 Ok(())
81 })
82 .await?;
83
84 Ok(())
85 }
86
87 #[instrument(skip(self))]
89 pub(crate) async fn update_image_local_id(
90 &self,
91 id: Uuid,
92 local_id: Option<String>,
93 ) -> Result<()> {
94 self.handle
95 .for_write(move |writer| {
96 let updated = update(Image::find_id(&SqlUuid::new(id)))
97 .set(images::local_id.eq(local_id))
98 .execute(writer)?;
99
100 HandleError::check_modified(updated, 1)?;
101
102 Ok(())
103 })
104 .await?;
105
106 Ok(())
107 }
108
109 #[instrument(skip(self))]
111 pub(crate) async fn delete_image(&self, id: Uuid) -> Result<()> {
112 self.handle
113 .for_write(move |writer| {
114 let updated = delete(Image::find_id(&SqlUuid::new(id))).execute(writer)?;
115
116 HandleError::check_modified(updated, 1)?;
117
118 Ok(())
119 })
120 .await?;
121
122 Ok(())
123 }
124
125 #[instrument(skip(self))]
127 pub(crate) async fn find_image(&self, id: Uuid) -> Result<Option<ImageResource>> {
128 let image = self
129 .handle
130 .for_read(move |reader| {
131 let image: Option<Image> =
132 Image::find_id(&SqlUuid::new(id)).first(reader).optional()?;
133
134 Ok(image)
135 })
136 .await?
137 .map(|img| ImageResource::new(ContainerImage::from(img)));
138
139 Ok(image)
140 }
141
142 #[instrument(skip(self))]
144 pub(crate) async fn load_images_to_publish(&self) -> Result<Vec<SqlUuid>> {
145 let image = self
146 .handle
147 .for_read(move |reader| {
148 let images = images::table
149 .select(images::id)
150 .filter(images::status.eq(ImageStatus::Received))
151 .load::<SqlUuid>(reader)?;
152
153 Ok(images)
154 })
155 .await?;
156
157 Ok(image)
158 }
159
160 #[instrument(skip(self))]
164 pub(crate) async fn find_image_by_local_id(
165 &self,
166 local_id: String,
167 ) -> Result<Option<(Uuid, String)>> {
168 let id = self
169 .handle
170 .for_read(|reader| {
171 images::table
172 .filter(images::local_id.eq(local_id))
173 .select((images::id, images::reference))
174 .first::<(SqlUuid, String)>(reader)
175 .map(|(id, reference)| (*id, reference))
176 .optional()
177 .map_err(HandleError::Query)
178 })
179 .await?;
180
181 Ok(id)
182 }
183}
184
185impl From<CreateImage> for Image {
186 fn from(
187 CreateImage {
188 id,
189 deployment_id: _,
190 reference,
191 registry_auth,
192 }: CreateImage,
193 ) -> Self {
194 let registry_auth = (!registry_auth.is_empty()).then_some(registry_auth);
195
196 Self {
197 id: SqlUuid::new(id),
198 local_id: None,
199 status: ImageStatus::default(),
200 reference,
201 registry_auth,
202 }
203 }
204}
205
206impl From<Image> for ContainerImage {
207 fn from(value: Image) -> Self {
208 Self::new(value.local_id, value.reference, value.registry_auth)
209 }
210}
211
212#[cfg(test)]
213mod tests {
214 use crate::requests::ReqUuid;
215
216 use super::*;
217
218 use edgehog_store::db;
219 use pretty_assertions::assert_eq;
220 use tempfile::TempDir;
221
222 async fn find_image(store: &StateStore, id: Uuid) -> Option<Image> {
223 store
224 .handle
225 .for_read(move |reader| {
226 Image::find_id(&SqlUuid::new(id))
227 .first::<Image>(reader)
228 .optional()
229 .map_err(HandleError::Query)
230 })
231 .await
232 .unwrap()
233 }
234
235 #[tokio::test]
236 async fn should_store() {
237 let tmp = TempDir::with_prefix("store_image").unwrap();
238 let db_file = tmp.path().join("state.db");
239 let db_file = db_file.to_str().unwrap();
240
241 let handle = db::Handle::open(db_file).await.unwrap();
242 let store = StateStore::new(handle);
243
244 let image_id = Uuid::new_v4();
245 let deployment_id = Uuid::new_v4();
246 let image = CreateImage {
247 id: ReqUuid(image_id),
248 deployment_id: ReqUuid(deployment_id),
249 reference: "postgres:15".to_string(),
250 registry_auth: String::new(),
251 };
252 store.create_image(image).await.unwrap();
253
254 let res = find_image(&store, image_id).await.unwrap();
255
256 let exp = Image {
257 id: SqlUuid::new(image_id),
258 local_id: None,
259 status: ImageStatus::Received,
260 reference: "postgres:15".to_string(),
261 registry_auth: None,
262 };
263
264 assert_eq!(res, exp)
265 }
266
267 #[tokio::test]
268 async fn should_update() {
269 let tmp = TempDir::with_prefix("update_image").unwrap();
270 let db_file = tmp.path().join("state.db");
271 let db_file = db_file.to_str().unwrap();
272
273 let handle = db::Handle::open(db_file).await.unwrap();
274 let store = StateStore::new(handle);
275
276 let image_id = Uuid::new_v4();
277 let deployment_id = Uuid::new_v4();
278 let image = CreateImage {
279 id: ReqUuid(image_id),
280 deployment_id: ReqUuid(deployment_id),
281 reference: "postgres:15".to_string(),
282 registry_auth: String::new(),
283 };
284 store.create_image(image).await.unwrap();
285 store
286 .update_image_status(image_id, ImageStatus::Published)
287 .await
288 .unwrap();
289 let local_id = Uuid::new_v4().to_string();
290 store
291 .update_image_local_id(image_id, Some(local_id.clone()))
292 .await
293 .unwrap();
294
295 let res = find_image(&store, image_id).await.unwrap();
296
297 let exp = Image {
298 id: SqlUuid::new(image_id),
299 local_id: Some(local_id),
300 status: ImageStatus::Published,
301 reference: "postgres:15".to_string(),
302 registry_auth: None,
303 };
304
305 assert_eq!(res, exp)
306 }
307
308 #[tokio::test]
309 async fn find_by_local_id() {
310 let tmp = TempDir::with_prefix("find_by_local_id").unwrap();
311 let db_file = tmp.path().join("state.db");
312 let db_file = db_file.to_str().unwrap();
313
314 let handle = db::Handle::open(db_file).await.unwrap();
315 let store = StateStore::new(handle);
316
317 let image_id = Uuid::new_v4();
318 let deployment_id = Uuid::new_v4();
319 let reference = "postgres:15".to_string();
320 let image = CreateImage {
321 id: ReqUuid(image_id),
322 deployment_id: ReqUuid(deployment_id),
323 reference: reference.clone(),
324 registry_auth: String::new(),
325 };
326 store.create_image(image).await.unwrap();
327 let local_id = Uuid::new_v4().to_string();
328 store
329 .update_image_local_id(image_id, Some(local_id.clone()))
330 .await
331 .unwrap();
332
333 let res = store
334 .find_image_by_local_id(local_id)
335 .await
336 .unwrap()
337 .unwrap();
338
339 assert_eq!(res, (image_id, reference))
340 }
341}