1use std::path::Path;
9
10use chrono::{DateTime, NaiveDateTime, Utc};
11use oci_spec::image::{ImageConfiguration, ImageIndex, ImageManifest, MediaType, Platform};
12use sqlx::{migrate::Migrator, sqlite::SqlitePoolOptions, Pool, Row, Sqlite};
13use tokio::fs;
14
15use crate::{
16 models::{Config, Image, Index, Layer, Manifest, Sandbox},
17 runtime::SANDBOX_STATUS_RUNNING,
18 MicrosandboxResult,
19};
20
21pub static SANDBOX_DB_MIGRATOR: Migrator = sqlx::migrate!("lib/migrations/sandbox");
27
28pub static OCI_DB_MIGRATOR: Migrator = sqlx::migrate!("lib/migrations/oci");
30
31pub async fn initialize(
42 db_path: impl AsRef<Path>,
43 migrator: &Migrator,
44) -> MicrosandboxResult<Pool<Sqlite>> {
45 let db_path = db_path.as_ref();
46
47 if let Some(parent) = db_path.parent() {
49 fs::create_dir_all(parent).await?;
50 }
51
52 if !db_path.exists() {
54 fs::File::create(&db_path).await?;
55 }
56
57 let pool = SqlitePoolOptions::new()
59 .max_connections(5)
60 .connect(&format!("sqlite://{}?mode=rwc", db_path.display()))
61 .await?;
62
63 migrator.run(&pool).await?;
65
66 Ok(pool)
67}
68
69pub async fn get_pool(db_path: impl AsRef<Path>) -> MicrosandboxResult<Pool<Sqlite>> {
75 let db_path = db_path.as_ref();
76 let pool = SqlitePoolOptions::new()
77 .max_connections(5)
78 .connect(&format!("sqlite://{}?mode=rwc", db_path.display()))
79 .await?;
80
81 Ok(pool)
82}
83
84pub async fn get_or_create_pool(
95 db_path: impl AsRef<Path>,
96 migrator: &Migrator,
97) -> MicrosandboxResult<Pool<Sqlite>> {
98 initialize(&db_path, migrator).await
100}
101
102pub(crate) async fn save_or_update_sandbox(
110 pool: &Pool<Sqlite>,
111 name: &str,
112 config_file: &str,
113 config_last_modified: &DateTime<Utc>,
114 status: &str,
115 supervisor_pid: u32,
116 microvm_pid: u32,
117 rootfs_paths: &str,
118 group_id: Option<u32>,
119 group_ip: Option<String>,
120) -> MicrosandboxResult<i64> {
121 let sandbox = Sandbox {
122 id: 0,
123 name: name.to_string(),
124 config_file: config_file.to_string(),
125 config_last_modified: config_last_modified.clone(),
126 status: status.to_string(),
127 supervisor_pid,
128 microvm_pid,
129 rootfs_paths: rootfs_paths.to_string(),
130 group_id,
131 group_ip,
132 created_at: Utc::now(),
133 modified_at: Utc::now(),
134 };
135
136 let update_result = sqlx::query(
138 r#"
139 UPDATE sandboxes
140 SET config_last_modified = ?,
141 status = ?,
142 supervisor_pid = ?,
143 microvm_pid = ?,
144 rootfs_paths = ?,
145 group_id = ?,
146 group_ip = ?,
147 modified_at = CURRENT_TIMESTAMP
148 WHERE name = ? AND config_file = ?
149 RETURNING id
150 "#,
151 )
152 .bind(&sandbox.config_last_modified.to_rfc3339())
153 .bind(&sandbox.status)
154 .bind(&sandbox.supervisor_pid)
155 .bind(&sandbox.microvm_pid)
156 .bind(&sandbox.rootfs_paths)
157 .bind(&sandbox.group_id)
158 .bind(&sandbox.group_ip)
159 .bind(&sandbox.name)
160 .bind(&sandbox.config_file)
161 .fetch_optional(pool)
162 .await?;
163
164 if let Some(record) = update_result {
165 tracing::debug!("updated existing sandbox record");
166 Ok(record.get::<i64, _>("id"))
167 } else {
168 tracing::debug!("creating new sandbox record");
170 let record = sqlx::query(
171 r#"
172 INSERT INTO sandboxes (
173 name, config_file, config_last_modified,
174 status, supervisor_pid, microvm_pid, rootfs_paths,
175 group_id, group_ip
176 )
177 VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
178 RETURNING id
179 "#,
180 )
181 .bind(sandbox.name)
182 .bind(sandbox.config_file)
183 .bind(sandbox.config_last_modified.to_rfc3339())
184 .bind(sandbox.status)
185 .bind(sandbox.supervisor_pid)
186 .bind(sandbox.microvm_pid)
187 .bind(sandbox.rootfs_paths)
188 .bind(sandbox.group_id)
189 .bind(sandbox.group_ip)
190 .fetch_one(pool)
191 .await?;
192
193 Ok(record.get::<i64, _>("id"))
194 }
195}
196
197pub(crate) async fn get_sandbox(
198 pool: &Pool<Sqlite>,
199 name: &str,
200 config_file: &str,
201) -> MicrosandboxResult<Option<Sandbox>> {
202 let record = sqlx::query(
203 r#"
204 SELECT id, name, config_file, config_last_modified, status,
205 supervisor_pid, microvm_pid, rootfs_paths,
206 group_id, group_ip, created_at, modified_at
207 FROM sandboxes
208 WHERE name = ? AND config_file = ?
209 "#,
210 )
211 .bind(name)
212 .bind(config_file)
213 .fetch_optional(pool)
214 .await?;
215
216 Ok(record.map(|row| Sandbox {
217 id: row.get("id"),
218 name: row.get("name"),
219 config_file: row.get("config_file"),
220 config_last_modified: row
221 .get::<String, _>("config_last_modified")
222 .parse::<DateTime<Utc>>()
223 .unwrap(),
224 status: row.get("status"),
225 supervisor_pid: row.get("supervisor_pid"),
226 microvm_pid: row.get("microvm_pid"),
227 rootfs_paths: row.get("rootfs_paths"),
228 group_id: row.get("group_id"),
229 group_ip: row.get("group_ip"),
230 created_at: parse_sqlite_datetime(&row.get::<String, _>("created_at")),
231 modified_at: parse_sqlite_datetime(&row.get::<String, _>("modified_at")),
232 }))
233}
234
235pub(crate) async fn update_sandbox_status(
237 pool: &Pool<Sqlite>,
238 name: &str,
239 config_file: &str,
240 status: &str,
241) -> MicrosandboxResult<()> {
242 sqlx::query(
243 r#"
244 UPDATE sandboxes
245 SET status = ?,
246 modified_at = CURRENT_TIMESTAMP
247 WHERE name = ? AND config_file = ?
248 "#,
249 )
250 .bind(status)
251 .bind(name)
252 .bind(config_file)
253 .execute(pool)
254 .await?;
255
256 Ok(())
257}
258
259pub(crate) async fn get_running_config_sandboxes(
261 pool: &Pool<Sqlite>,
262 config_file: &str,
263) -> MicrosandboxResult<Vec<Sandbox>> {
264 let records = sqlx::query(
265 r#"
266 SELECT id, name, config_file, config_last_modified, status,
267 supervisor_pid, microvm_pid, rootfs_paths,
268 group_id, group_ip, created_at, modified_at
269 FROM sandboxes
270 WHERE config_file = ? AND status = ?
271 ORDER BY created_at DESC
272 "#,
273 )
274 .bind(config_file)
275 .bind(SANDBOX_STATUS_RUNNING)
276 .fetch_all(pool)
277 .await?;
278
279 Ok(records
280 .into_iter()
281 .map(|row| Sandbox {
282 id: row.get("id"),
283 name: row.get("name"),
284 config_file: row.get("config_file"),
285 config_last_modified: row
286 .get::<String, _>("config_last_modified")
287 .parse::<DateTime<Utc>>()
288 .unwrap(),
289 status: row.get("status"),
290 supervisor_pid: row.get("supervisor_pid"),
291 microvm_pid: row.get("microvm_pid"),
292 rootfs_paths: row.get("rootfs_paths"),
293 group_id: row.get("group_id"),
294 group_ip: row.get("group_ip"),
295 created_at: parse_sqlite_datetime(&row.get::<String, _>("created_at")),
296 modified_at: parse_sqlite_datetime(&row.get::<String, _>("modified_at")),
297 })
298 .collect())
299}
300
301pub(crate) async fn delete_sandbox(
303 pool: &Pool<Sqlite>,
304 name: &str,
305 config_file: &str,
306) -> MicrosandboxResult<()> {
307 sqlx::query(
308 r#"
309 DELETE FROM sandboxes
310 WHERE name = ? AND config_file = ?
311 "#,
312 )
313 .bind(name)
314 .bind(config_file)
315 .execute(pool)
316 .await?;
317
318 Ok(())
319}
320
321pub(crate) async fn save_image(
327 pool: &Pool<Sqlite>,
328 reference: &str,
329 size_bytes: i64,
330) -> MicrosandboxResult<i64> {
331 let image = Image {
332 id: 0, reference: reference.to_string(),
334 size_bytes,
335 last_used_at: Some(Utc::now()),
336 created_at: Utc::now(),
337 modified_at: Utc::now(),
338 };
339
340 let record = sqlx::query(
341 r#"
342 INSERT INTO images (reference, size_bytes, last_used_at)
343 VALUES (?, ?, CURRENT_TIMESTAMP)
344 RETURNING id
345 "#,
346 )
347 .bind(&image.reference)
348 .bind(image.size_bytes)
349 .fetch_one(pool)
350 .await?;
351
352 Ok(record.get::<i64, _>("id"))
353}
354
355pub(crate) async fn save_index(
357 pool: &Pool<Sqlite>,
358 image_id: i64,
359 index: &ImageIndex,
360 platform: Option<&Platform>,
361) -> MicrosandboxResult<i64> {
362 let index_model = Index {
363 id: 0, image_id,
365 schema_version: index.schema_version() as i64,
366 media_type: index
367 .media_type()
368 .as_ref()
369 .map(|mt| mt.to_string())
370 .unwrap_or_else(|| MediaType::ImageIndex.to_string()),
371 platform_os: platform.map(|p| p.os().to_string()),
372 platform_arch: platform.map(|p| p.architecture().to_string()),
373 platform_variant: platform.and_then(|p| p.variant().as_ref().map(|v| v.to_string())),
374 annotations_json: index
375 .annotations()
376 .as_ref()
377 .map(|a| serde_json::to_string(a).unwrap_or_default()),
378 created_at: Utc::now(),
379 modified_at: Utc::now(),
380 };
381
382 let record = sqlx::query(
383 r#"
384 INSERT INTO indexes (
385 image_id, schema_version, media_type,
386 platform_os, platform_arch, platform_variant,
387 annotations_json
388 )
389 VALUES (?, ?, ?, ?, ?, ?, ?)
390 RETURNING id
391 "#,
392 )
393 .bind(index_model.image_id)
394 .bind(index_model.schema_version)
395 .bind(&index_model.media_type)
396 .bind(&index_model.platform_os)
397 .bind(&index_model.platform_arch)
398 .bind(&index_model.platform_variant)
399 .bind(&index_model.annotations_json)
400 .fetch_one(pool)
401 .await?;
402
403 Ok(record.get::<i64, _>("id"))
404}
405
406pub(crate) async fn save_manifest(
408 pool: &Pool<Sqlite>,
409 image_id: i64,
410 index_id: Option<i64>,
411 manifest: &ImageManifest,
412) -> MicrosandboxResult<i64> {
413 let manifest_model = Manifest {
414 id: 0, index_id,
416 image_id,
417 schema_version: manifest.schema_version() as i64,
418 media_type: manifest
419 .media_type()
420 .as_ref()
421 .map(|mt| mt.to_string())
422 .unwrap_or_else(|| MediaType::ImageManifest.to_string()),
423 annotations_json: manifest
424 .annotations()
425 .as_ref()
426 .map(|a| serde_json::to_string(a).unwrap_or_default()),
427 created_at: Utc::now(),
428 modified_at: Utc::now(),
429 };
430
431 let record = sqlx::query(
432 r#"
433 INSERT INTO manifests (
434 index_id, image_id, schema_version,
435 media_type, annotations_json
436 )
437 VALUES (?, ?, ?, ?, ?)
438 RETURNING id
439 "#,
440 )
441 .bind(manifest_model.index_id)
442 .bind(manifest_model.image_id)
443 .bind(manifest_model.schema_version)
444 .bind(&manifest_model.media_type)
445 .bind(&manifest_model.annotations_json)
446 .fetch_one(pool)
447 .await?;
448
449 Ok(record.get::<i64, _>("id"))
450}
451
452pub(crate) async fn save_config(
454 pool: &Pool<Sqlite>,
455 manifest_id: i64,
456 config: &ImageConfiguration,
457) -> MicrosandboxResult<i64> {
458 let config_model = Config {
459 id: 0, manifest_id,
461 media_type: MediaType::ImageConfig.to_string(),
462 created: config
463 .created()
464 .as_ref()
465 .map(|dt| dt.parse::<DateTime<Utc>>().unwrap()),
466 architecture: config.architecture().to_string(),
467 os: config.os().to_string(),
468 os_variant: config.os_version().as_ref().map(|s| s.to_string()),
469 config_env_json: config
470 .config()
471 .as_ref()
472 .map(|c| serde_json::to_string(c.env()).unwrap_or_default()),
473 config_cmd_json: config
474 .config()
475 .as_ref()
476 .map(|c| serde_json::to_string(c.cmd()).unwrap_or_default()),
477 config_working_dir: config
478 .config()
479 .as_ref()
480 .and_then(|c| c.working_dir().as_ref().map(String::from)),
481 config_entrypoint_json: config
482 .config()
483 .as_ref()
484 .map(|c| serde_json::to_string(c.entrypoint()).unwrap_or_default()),
485 config_volumes_json: config
486 .config()
487 .as_ref()
488 .map(|c| serde_json::to_string(c.volumes()).unwrap_or_default()),
489 config_exposed_ports_json: config
490 .config()
491 .as_ref()
492 .map(|c| serde_json::to_string(c.exposed_ports()).unwrap_or_default()),
493 config_user: config
494 .config()
495 .as_ref()
496 .and_then(|c| c.user().as_ref().map(String::from)),
497 rootfs_type: config.rootfs().typ().to_string(),
498 rootfs_diff_ids_json: Some(
499 serde_json::to_string(&config.rootfs().diff_ids()).unwrap_or_default(),
500 ),
501 history_json: Some(serde_json::to_string(config.history()).unwrap_or_default()),
502 created_at: Utc::now(),
503 modified_at: Utc::now(),
504 };
505
506 let record = sqlx::query(
507 r#"
508 INSERT INTO configs (
509 manifest_id, media_type, created, architecture,
510 os, os_variant, config_env_json, config_cmd_json,
511 config_working_dir, config_entrypoint_json,
512 config_volumes_json, config_exposed_ports_json,
513 config_user, rootfs_type, rootfs_diff_ids_json,
514 history_json
515 )
516 VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
517 RETURNING id
518 "#,
519 )
520 .bind(config_model.manifest_id)
521 .bind(&config_model.media_type)
522 .bind(config_model.created.map(|dt| dt.to_rfc3339()))
523 .bind(&config_model.architecture)
524 .bind(&config_model.os)
525 .bind(&config_model.os_variant)
526 .bind(&config_model.config_env_json)
527 .bind(&config_model.config_cmd_json)
528 .bind(&config_model.config_working_dir)
529 .bind(&config_model.config_entrypoint_json)
530 .bind(&config_model.config_volumes_json)
531 .bind(&config_model.config_exposed_ports_json)
532 .bind(&config_model.config_user)
533 .bind(&config_model.rootfs_type)
534 .bind(&config_model.rootfs_diff_ids_json)
535 .bind(&config_model.history_json)
536 .fetch_one(pool)
537 .await?;
538
539 Ok(record.get::<i64, _>("id"))
540}
541
542pub(crate) async fn save_layer(
544 pool: &Pool<Sqlite>,
545 media_type: &str,
546 digest: &str,
547 size_bytes: i64,
548 diff_id: &str,
549) -> MicrosandboxResult<i64> {
550 let layer_model = Layer {
551 id: 0, media_type: media_type.to_string(),
553 digest: digest.to_string(),
554 diff_id: diff_id.to_string(),
555 size_bytes,
556 created_at: Utc::now(),
557 modified_at: Utc::now(),
558 };
559
560 let record = sqlx::query(
561 r#"
562 INSERT INTO layers (
563 media_type, digest, size_bytes, diff_id
564 )
565 VALUES (?, ?, ?, ?)
566 RETURNING id
567 "#,
568 )
569 .bind(&layer_model.media_type)
570 .bind(&layer_model.digest)
571 .bind(layer_model.size_bytes)
572 .bind(&layer_model.diff_id)
573 .fetch_one(pool)
574 .await?;
575
576 Ok(record.get::<i64, _>("id"))
577}
578
579pub(crate) async fn save_or_update_layer(
583 pool: &Pool<Sqlite>,
584 media_type: &str,
585 digest: &str,
586 size_bytes: i64,
587 diff_id: &str,
588) -> MicrosandboxResult<i64> {
589 let layer_model = Layer {
590 id: 0, media_type: media_type.to_string(),
592 digest: digest.to_string(),
593 diff_id: diff_id.to_string(),
594 size_bytes,
595 created_at: Utc::now(),
596 modified_at: Utc::now(),
597 };
598
599 let update_result = sqlx::query(
601 r#"
602 UPDATE layers
603 SET media_type = ?,
604 size_bytes = ?,
605 diff_id = ?,
606 modified_at = CURRENT_TIMESTAMP
607 WHERE digest = ?
608 RETURNING id
609 "#,
610 )
611 .bind(&layer_model.media_type)
612 .bind(layer_model.size_bytes)
613 .bind(&layer_model.diff_id)
614 .bind(&layer_model.digest)
615 .fetch_optional(pool)
616 .await?;
617
618 if let Some(record) = update_result {
619 Ok(record.get::<i64, _>("id"))
620 } else {
621 save_layer(pool, media_type, digest, size_bytes, diff_id).await
623 }
624}
625
626pub(crate) async fn save_manifest_layer(
628 pool: &Pool<Sqlite>,
629 manifest_id: i64,
630 layer_id: i64,
631) -> MicrosandboxResult<i64> {
632 let record = sqlx::query(
633 r#"
634 INSERT INTO manifest_layers (manifest_id, layer_id)
635 VALUES (?, ?)
636 ON CONFLICT (manifest_id, layer_id) DO NOTHING
637 RETURNING id
638 "#,
639 )
640 .bind(manifest_id)
641 .bind(layer_id)
642 .fetch_optional(pool)
643 .await?;
644
645 if let Some(record) = record {
646 Ok(record.get::<i64, _>("id"))
647 } else {
648 let record = sqlx::query(
650 r#"
651 SELECT id FROM manifest_layers
652 WHERE manifest_id = ? AND layer_id = ?
653 "#,
654 )
655 .bind(manifest_id)
656 .bind(layer_id)
657 .fetch_one(pool)
658 .await?;
659
660 Ok(record.get::<i64, _>("id"))
661 }
662}
663
664pub async fn get_image_layers(
666 pool: &Pool<Sqlite>,
667 reference: &str,
668) -> MicrosandboxResult<Vec<Layer>> {
669 let records = sqlx::query(
670 r#"
671 SELECT l.id, l.media_type, l.digest,
672 l.diff_id, l.size_bytes, l.created_at, l.modified_at
673 FROM layers l
674 JOIN manifest_layers ml ON l.id = ml.layer_id
675 JOIN manifests m ON ml.manifest_id = m.id
676 JOIN images i ON m.image_id = i.id
677 WHERE i.reference = ?
678 ORDER BY l.id ASC
679 "#,
680 )
681 .bind(reference)
682 .fetch_all(pool)
683 .await?;
684
685 Ok(records
686 .into_iter()
687 .map(|row| Layer {
688 id: row.get("id"),
689 media_type: row.get("media_type"),
690 digest: row.get("digest"),
691 diff_id: row.get("diff_id"),
692 size_bytes: row.get("size_bytes"),
693 created_at: parse_sqlite_datetime(&row.get::<String, _>("created_at")),
694 modified_at: parse_sqlite_datetime(&row.get::<String, _>("modified_at")),
695 })
696 .collect())
697}
698
699pub(crate) async fn image_exists(pool: &Pool<Sqlite>, reference: &str) -> MicrosandboxResult<bool> {
701 let record = sqlx::query(
702 r#"
703 SELECT COUNT(*) as count
704 FROM images
705 WHERE reference = ?
706 "#,
707 )
708 .bind(reference)
709 .fetch_one(pool)
710 .await?;
711
712 Ok(record.get::<i64, _>("count") > 0)
713}
714
715pub(crate) async fn get_image_config(
730 pool: &Pool<Sqlite>,
731 reference: &str,
732) -> MicrosandboxResult<Option<Config>> {
733 let record = sqlx::query(
734 r#"
735 SELECT c.id, c.manifest_id, c.media_type, c.created, c.architecture,
736 c.os, c.os_variant, c.config_env_json, c.config_cmd_json,
737 c.config_working_dir, c.config_entrypoint_json,
738 c.config_volumes_json, c.config_exposed_ports_json,
739 c.config_user, c.rootfs_type, c.rootfs_diff_ids_json,
740 c.history_json, c.created_at, c.modified_at
741 FROM configs c
742 JOIN manifests m ON c.manifest_id = m.id
743 JOIN images i ON m.image_id = i.id
744 WHERE i.reference = ?
745 LIMIT 1
746 "#,
747 )
748 .bind(reference)
749 .fetch_optional(pool)
750 .await?;
751
752 Ok(record.map(|row| Config {
753 id: row.get("id"),
754 manifest_id: row.get("manifest_id"),
755 media_type: row.get("media_type"),
756 created: row
757 .get::<Option<String>, _>("created")
758 .map(|dt| dt.parse::<DateTime<Utc>>().unwrap()),
759 architecture: row.get("architecture"),
760 os: row.get("os"),
761 os_variant: row.get("os_variant"),
762 config_env_json: null_to_none(row.get("config_env_json")),
763 config_cmd_json: null_to_none(row.get("config_cmd_json")),
764 config_working_dir: row.get("config_working_dir"),
765 config_entrypoint_json: null_to_none(row.get("config_entrypoint_json")),
766 config_volumes_json: null_to_none(row.get("config_volumes_json")),
767 config_exposed_ports_json: null_to_none(row.get("config_exposed_ports_json")),
768 config_user: row.get("config_user"),
769 rootfs_type: row.get("rootfs_type"),
770 rootfs_diff_ids_json: row.get("rootfs_diff_ids_json"),
771 history_json: null_to_none(row.get("history_json")),
772 created_at: parse_sqlite_datetime(&row.get::<String, _>("created_at")),
773 modified_at: parse_sqlite_datetime(&row.get::<String, _>("modified_at")),
774 }))
775}
776
777pub(crate) async fn save_or_update_image(
781 pool: &Pool<Sqlite>,
782 reference: &str,
783 size_bytes: i64,
784) -> MicrosandboxResult<i64> {
785 let update_result = sqlx::query(
787 r#"
788 UPDATE images
789 SET size_bytes = ?, last_used_at = CURRENT_TIMESTAMP, modified_at = CURRENT_TIMESTAMP
790 WHERE reference = ?
791 RETURNING id, reference, size_bytes, last_used_at, created_at, modified_at
792 "#,
793 )
794 .bind(size_bytes)
795 .bind(reference)
796 .fetch_optional(pool)
797 .await?;
798
799 if let Some(record) = update_result {
800 Ok(record.get::<i64, _>("id"))
801 } else {
802 save_image(pool, reference, size_bytes).await
804 }
805}
806
807pub(crate) async fn get_layers_by_digest(
822 pool: &Pool<Sqlite>,
823 digests: &[String],
824) -> MicrosandboxResult<Vec<Layer>> {
825 if digests.is_empty() {
826 return Ok(Vec::new());
827 }
828
829 let placeholders = (0..digests.len())
831 .map(|_| "?")
832 .collect::<Vec<_>>()
833 .join(",");
834
835 let query = format!(
836 r#"
837 SELECT id, media_type, digest, diff_id, size_bytes, created_at, modified_at
838 FROM layers
839 WHERE digest IN ({})
840 "#,
841 placeholders
842 );
843
844 let mut query_builder = sqlx::query(&query);
846 for digest in digests {
847 query_builder = query_builder.bind(digest);
848 }
849
850 let records = query_builder.fetch_all(pool).await?;
851
852 Ok(records
853 .into_iter()
854 .map(|row| Layer {
855 id: row.get("id"),
856 media_type: row.get("media_type"),
857 digest: row.get("digest"),
858 diff_id: row.get("diff_id"),
859 size_bytes: row.get("size_bytes"),
860 created_at: parse_sqlite_datetime(&row.get::<String, _>("created_at")),
861 modified_at: parse_sqlite_datetime(&row.get::<String, _>("modified_at")),
862 })
863 .collect())
864}
865
866pub(crate) async fn get_image_layer_digests(
880 pool: &Pool<Sqlite>,
881 reference: &str,
882) -> MicrosandboxResult<Vec<String>> {
883 let records = sqlx::query(
884 r#"
885 SELECT l.digest
886 FROM layers l
887 JOIN manifest_layers ml ON l.id = ml.layer_id
888 JOIN manifests m ON ml.manifest_id = m.id
889 JOIN images i ON m.image_id = i.id
890 WHERE i.reference = ?
891 ORDER BY l.id ASC
892 "#,
893 )
894 .bind(reference)
895 .fetch_all(pool)
896 .await?;
897
898 Ok(records
899 .into_iter()
900 .map(|row| row.get::<String, _>("digest"))
901 .collect())
902}
903
904#[cfg(test)]
909mod tests {
910 use super::*;
911 use sqlx::Row;
912 use tempfile::tempdir;
913
914 #[tokio::test]
915 async fn test_init_sandbox_db() -> MicrosandboxResult<()> {
916 let temp_dir = tempdir()?;
918 let db_path = temp_dir.path().join("test_sandbox.db");
919
920 initialize(&db_path, &SANDBOX_DB_MIGRATOR).await?;
922
923 let pool = get_pool(&db_path).await?;
925
926 let tables = sqlx::query("SELECT name FROM sqlite_master WHERE type='table'")
928 .fetch_all(&pool)
929 .await?;
930
931 let table_names: Vec<String> = tables
932 .iter()
933 .map(|row| row.get::<String, _>("name"))
934 .collect();
935
936 assert!(
937 table_names.contains(&"sandboxes".to_string()),
938 "sandboxes table not found"
939 );
940 assert!(
941 table_names.contains(&"groups".to_string()),
942 "groups table not found"
943 );
944 assert!(
945 table_names.contains(&"sandbox_metrics".to_string()),
946 "sandbox_metrics table not found"
947 );
948
949 Ok(())
950 }
951
952 #[tokio::test]
953 async fn test_init_oci_db() -> MicrosandboxResult<()> {
954 let temp_dir = tempdir()?;
956 let db_path = temp_dir.path().join("test_oci.db");
957
958 initialize(&db_path, &OCI_DB_MIGRATOR).await?;
960
961 let pool = get_pool(&db_path).await?;
963
964 let tables = sqlx::query("SELECT name FROM sqlite_master WHERE type='table'")
966 .fetch_all(&pool)
967 .await?;
968
969 let table_names: Vec<String> = tables
970 .iter()
971 .map(|row| row.get::<String, _>("name"))
972 .collect();
973
974 assert!(
975 table_names.contains(&"images".to_string()),
976 "images table not found"
977 );
978 assert!(
979 table_names.contains(&"indexes".to_string()),
980 "indexes table not found"
981 );
982 assert!(
983 table_names.contains(&"manifests".to_string()),
984 "manifests table not found"
985 );
986 assert!(
987 table_names.contains(&"configs".to_string()),
988 "configs table not found"
989 );
990 assert!(
991 table_names.contains(&"layers".to_string()),
992 "layers table not found"
993 );
994
995 Ok(())
996 }
997}
998
999fn parse_sqlite_datetime(s: &str) -> DateTime<Utc> {
1005 let naive_dt = NaiveDateTime::parse_from_str(s, "%Y-%m-%d %H:%M:%S")
1006 .unwrap_or_else(|e| panic!("Failed to parse datetime string '{}': {:?}", s, e));
1007 DateTime::from_naive_utc_and_offset(naive_dt, Utc)
1008}
1009
1010fn null_to_none(value: Option<String>) -> Option<String> {
1013 value.filter(|v| v != "null")
1014}