1use std::collections::HashMap;
7use std::path::{Path, PathBuf};
8
9use chrono::{DateTime, Utc};
10use serde::{Deserialize, Serialize};
11use uuid::Uuid;
12
13use crate::error::{OciError, Result};
14
15#[derive(Debug, Clone, Serialize, Deserialize)]
20#[serde(rename_all = "camelCase")]
21pub struct State {
22 pub oci_version: String,
24
25 pub id: String,
27
28 pub status: Status,
30
31 #[serde(skip_serializing_if = "Option::is_none")]
33 pub pid: Option<u32>,
34
35 pub bundle: PathBuf,
37
38 #[serde(default, skip_serializing_if = "HashMap::is_empty")]
40 pub annotations: HashMap<String, String>,
41}
42
43impl State {
44 #[must_use]
46 pub fn new(id: String, bundle: PathBuf) -> Self {
47 Self {
48 oci_version: crate::config::OCI_VERSION.to_string(),
49 id,
50 status: Status::Creating,
51 pid: None,
52 bundle,
53 annotations: HashMap::new(),
54 }
55 }
56
57 #[must_use]
59 pub fn with_generated_id(bundle: PathBuf) -> Self {
60 Self::new(Uuid::new_v4().to_string(), bundle)
61 }
62
63 pub fn load<P: AsRef<Path>>(path: P) -> Result<Self> {
65 let content = std::fs::read_to_string(path)?;
66 Ok(serde_json::from_str(&content)?)
67 }
68
69 pub fn save<P: AsRef<Path>>(&self, path: P) -> Result<()> {
71 let json = serde_json::to_string_pretty(self)?;
72 std::fs::write(path, json)?;
73 Ok(())
74 }
75
76 pub fn to_json(&self) -> Result<String> {
78 Ok(serde_json::to_string_pretty(self)?)
79 }
80
81 pub const fn set_pid(&mut self, pid: u32) {
83 self.pid = Some(pid);
84 }
85
86 pub const fn clear_pid(&mut self) {
88 self.pid = None;
89 }
90
91 pub fn transition_to(&mut self, new_status: Status) -> Result<()> {
95 if !self.status.can_transition_to(new_status) {
96 return Err(OciError::Common(arcbox_error::CommonError::invalid_state(
97 format!(
98 "expected one of [{}], got {}",
99 self.status.valid_transitions().join(", "),
100 new_status.as_str()
101 ),
102 )));
103 }
104 self.status = new_status;
105 Ok(())
106 }
107}
108
109#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
117#[serde(rename_all = "lowercase")]
118pub enum Status {
119 Creating,
121 Created,
123 Running,
125 Stopped,
127}
128
129impl Status {
130 #[must_use]
132 pub const fn as_str(&self) -> &'static str {
133 match self {
134 Self::Creating => "creating",
135 Self::Created => "created",
136 Self::Running => "running",
137 Self::Stopped => "stopped",
138 }
139 }
140
141 #[must_use]
143 pub const fn can_transition_to(&self, target: Self) -> bool {
144 matches!(
145 (self, target),
146 (Self::Creating, Self::Created | Self::Stopped)
147 | (Self::Created, Self::Running | Self::Stopped)
148 | (Self::Running, Self::Stopped)
149 )
150 }
151
152 #[must_use]
154 pub fn valid_transitions(&self) -> Vec<&'static str> {
155 match self {
156 Self::Creating => vec!["created", "stopped"],
157 Self::Created => vec!["running", "stopped"],
158 Self::Running => vec!["stopped"],
159 Self::Stopped => vec![],
160 }
161 }
162
163 #[must_use]
165 pub const fn is_running(&self) -> bool {
166 matches!(self, Self::Running)
167 }
168
169 #[must_use]
171 pub const fn can_start(&self) -> bool {
172 matches!(self, Self::Created)
173 }
174
175 #[must_use]
177 pub const fn can_kill(&self) -> bool {
178 matches!(self, Self::Created | Self::Running)
179 }
180
181 #[must_use]
183 pub const fn can_delete(&self) -> bool {
184 matches!(self, Self::Stopped)
185 }
186}
187
188impl std::fmt::Display for Status {
189 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
190 write!(f, "{}", self.as_str())
191 }
192}
193
194impl std::str::FromStr for Status {
195 type Err = OciError;
196
197 fn from_str(s: &str) -> Result<Self> {
198 match s.to_lowercase().as_str() {
199 "creating" => Ok(Self::Creating),
200 "created" => Ok(Self::Created),
201 "running" => Ok(Self::Running),
202 "stopped" => Ok(Self::Stopped),
203 _ => Err(OciError::InvalidConfig(format!("unknown status: {s}"))),
204 }
205 }
206}
207
208#[derive(Debug, Clone, Serialize, Deserialize)]
212#[serde(rename_all = "camelCase")]
213pub struct ContainerState {
214 #[serde(flatten)]
216 pub oci_state: State,
217
218 pub created: DateTime<Utc>,
220
221 #[serde(skip_serializing_if = "Option::is_none")]
223 pub started: Option<DateTime<Utc>>,
224
225 #[serde(skip_serializing_if = "Option::is_none")]
227 pub finished: Option<DateTime<Utc>>,
228
229 #[serde(skip_serializing_if = "Option::is_none")]
231 pub exit_code: Option<i32>,
232
233 #[serde(skip_serializing_if = "Option::is_none")]
235 pub name: Option<String>,
236
237 #[serde(skip_serializing_if = "Option::is_none")]
239 pub image: Option<String>,
240
241 pub rootfs: PathBuf,
243}
244
245impl ContainerState {
246 #[must_use]
248 pub fn new(id: String, bundle: PathBuf, rootfs: PathBuf) -> Self {
249 Self {
250 oci_state: State::new(id, bundle),
251 created: Utc::now(),
252 started: None,
253 finished: None,
254 exit_code: None,
255 name: None,
256 image: None,
257 rootfs,
258 }
259 }
260
261 #[must_use]
263 pub fn id(&self) -> &str {
264 &self.oci_state.id
265 }
266
267 #[must_use]
269 pub const fn status(&self) -> Status {
270 self.oci_state.status
271 }
272
273 #[must_use]
275 pub fn bundle(&self) -> &Path {
276 &self.oci_state.bundle
277 }
278
279 pub fn mark_created(&mut self) -> Result<()> {
281 self.oci_state.transition_to(Status::Created)
282 }
283
284 pub fn mark_started(&mut self, pid: u32) -> Result<()> {
286 self.oci_state.set_pid(pid);
287 self.oci_state.transition_to(Status::Running)?;
288 self.started = Some(Utc::now());
289 Ok(())
290 }
291
292 pub fn mark_stopped(&mut self, exit_code: i32) -> Result<()> {
294 self.oci_state.clear_pid();
295 self.oci_state.transition_to(Status::Stopped)?;
296 self.finished = Some(Utc::now());
297 self.exit_code = Some(exit_code);
298 Ok(())
299 }
300
301 #[must_use]
303 pub const fn oci_state(&self) -> &State {
304 &self.oci_state
305 }
306
307 pub fn load<P: AsRef<Path>>(path: P) -> Result<Self> {
309 let content = std::fs::read_to_string(path)?;
310 Ok(serde_json::from_str(&content)?)
311 }
312
313 pub fn save<P: AsRef<Path>>(&self, path: P) -> Result<()> {
315 let json = serde_json::to_string_pretty(self)?;
316 std::fs::write(path, json)?;
317 Ok(())
318 }
319}
320
321pub struct StateStore {
325 root: PathBuf,
327}
328
329impl StateStore {
330 pub fn new<P: Into<PathBuf>>(root: P) -> Result<Self> {
332 let root = root.into();
333 std::fs::create_dir_all(&root)?;
334 Ok(Self { root })
335 }
336
337 fn state_path(&self, id: &str) -> PathBuf {
339 self.root.join(id).join("state.json")
340 }
341
342 fn container_dir(&self, id: &str) -> PathBuf {
344 self.root.join(id)
345 }
346
347 pub fn save(&self, state: &ContainerState) -> Result<()> {
349 let dir = self.container_dir(state.id());
350 std::fs::create_dir_all(&dir)?;
351 state.save(self.state_path(state.id()))
352 }
353
354 pub fn load(&self, id: &str) -> Result<ContainerState> {
356 let path = self.state_path(id);
357 if !path.exists() {
358 return Err(OciError::ContainerNotFound(id.to_string()));
359 }
360 ContainerState::load(path)
361 }
362
363 #[must_use]
365 pub fn exists(&self, id: &str) -> bool {
366 self.state_path(id).exists()
367 }
368
369 pub fn delete(&self, id: &str) -> Result<()> {
371 let dir = self.container_dir(id);
372 if dir.exists() {
373 std::fs::remove_dir_all(dir)?;
374 }
375 Ok(())
376 }
377
378 pub fn list(&self) -> Result<Vec<String>> {
380 let mut ids = Vec::new();
381 if self.root.exists() {
382 for entry in std::fs::read_dir(&self.root)? {
383 let entry = entry?;
384 if entry.file_type()?.is_dir() {
385 if let Some(name) = entry.file_name().to_str() {
386 if self.state_path(name).exists() {
387 ids.push(name.to_string());
388 }
389 }
390 }
391 }
392 }
393 Ok(ids)
394 }
395
396 pub fn list_states(&self) -> Result<Vec<ContainerState>> {
398 let ids = self.list()?;
399 let mut states = Vec::with_capacity(ids.len());
400 for id in ids {
401 states.push(self.load(&id)?);
402 }
403 Ok(states)
404 }
405}
406
407#[cfg(test)]
408mod tests {
409 use super::*;
410
411 #[test]
412 fn test_status_transitions() {
413 assert!(Status::Creating.can_transition_to(Status::Created));
414 assert!(Status::Created.can_transition_to(Status::Running));
415 assert!(Status::Running.can_transition_to(Status::Stopped));
416
417 assert!(!Status::Creating.can_transition_to(Status::Running));
418 assert!(!Status::Stopped.can_transition_to(Status::Running));
419 }
420
421 #[test]
422 fn test_state_transition() {
423 let mut state = State::new("test".to_string(), PathBuf::from("/bundle"));
424
425 assert_eq!(state.status, Status::Creating);
426 assert!(state.transition_to(Status::Created).is_ok());
427 assert_eq!(state.status, Status::Created);
428 assert!(state.transition_to(Status::Running).is_ok());
429 assert_eq!(state.status, Status::Running);
430 }
431
432 #[test]
433 fn test_invalid_transition() {
434 let mut state = State::new("test".to_string(), PathBuf::from("/bundle"));
435 assert!(state.transition_to(Status::Running).is_err());
436 }
437
438 #[test]
439 fn test_state_serialization() {
440 let state = State::new("test-container".to_string(), PathBuf::from("/var/run/test"));
441
442 let json = state.to_json().unwrap();
443 assert!(json.contains("test-container"));
444 assert!(json.contains("creating"));
445 }
446
447 #[test]
448 fn test_container_state_lifecycle() {
449 let mut state = ContainerState::new(
450 "test".to_string(),
451 PathBuf::from("/bundle"),
452 PathBuf::from("/rootfs"),
453 );
454
455 assert_eq!(state.status(), Status::Creating);
456 assert!(state.mark_created().is_ok());
457 assert_eq!(state.status(), Status::Created);
458 assert!(state.mark_started(1234).is_ok());
459 assert_eq!(state.status(), Status::Running);
460 assert!(state.started.is_some());
461 assert!(state.mark_stopped(0).is_ok());
462 assert_eq!(state.status(), Status::Stopped);
463 assert!(state.finished.is_some());
464 assert_eq!(state.exit_code, Some(0));
465 }
466
467 #[test]
468 fn test_status_from_str() {
469 assert_eq!("creating".parse::<Status>().unwrap(), Status::Creating);
470 assert_eq!("RUNNING".parse::<Status>().unwrap(), Status::Running);
471 assert!("invalid".parse::<Status>().is_err());
472 }
473
474 #[test]
475 fn test_status_display() {
476 assert_eq!(Status::Creating.to_string(), "creating");
477 assert_eq!(Status::Created.to_string(), "created");
478 assert_eq!(Status::Running.to_string(), "running");
479 assert_eq!(Status::Stopped.to_string(), "stopped");
480 }
481
482 #[test]
483 fn test_status_helper_methods() {
484 assert!(!Status::Creating.is_running());
485 assert!(Status::Running.is_running());
486
487 assert!(!Status::Creating.can_start());
488 assert!(Status::Created.can_start());
489 assert!(!Status::Running.can_start());
490
491 assert!(!Status::Creating.can_kill());
492 assert!(Status::Created.can_kill());
493 assert!(Status::Running.can_kill());
494 assert!(!Status::Stopped.can_kill());
495
496 assert!(!Status::Creating.can_delete());
497 assert!(!Status::Running.can_delete());
498 assert!(Status::Stopped.can_delete());
499 }
500
501 #[test]
502 fn test_status_valid_transitions() {
503 assert_eq!(
504 Status::Creating.valid_transitions(),
505 vec!["created", "stopped"]
506 );
507 assert_eq!(
508 Status::Created.valid_transitions(),
509 vec!["running", "stopped"]
510 );
511 assert_eq!(Status::Running.valid_transitions(), vec!["stopped"]);
512 assert!(Status::Stopped.valid_transitions().is_empty());
513 }
514
515 #[test]
516 fn test_state_pid_operations() {
517 let mut state = State::new("test".to_string(), PathBuf::from("/bundle"));
518 assert!(state.pid.is_none());
519
520 state.set_pid(1234);
521 assert_eq!(state.pid, Some(1234));
522
523 state.clear_pid();
524 assert!(state.pid.is_none());
525 }
526
527 #[test]
528 fn test_state_with_generated_id() {
529 let state = State::with_generated_id(PathBuf::from("/bundle"));
530 assert!(!state.id.is_empty());
531 assert_eq!(state.id.len(), 36);
533 assert!(state.id.contains('-'));
534 }
535
536 #[test]
537 fn test_state_annotations() {
538 let mut state = State::new("test".to_string(), PathBuf::from("/bundle"));
539 assert!(state.annotations.is_empty());
540
541 state
542 .annotations
543 .insert("key1".to_string(), "value1".to_string());
544 state
545 .annotations
546 .insert("key2".to_string(), "value2".to_string());
547
548 assert_eq!(state.annotations.len(), 2);
549 assert_eq!(state.annotations.get("key1"), Some(&"value1".to_string()));
550 }
551
552 #[test]
553 fn test_state_file_operations() {
554 let dir = tempfile::tempdir().unwrap();
555 let state_path = dir.path().join("state.json");
556
557 let state = State::new("test-container".to_string(), PathBuf::from("/bundle"));
558 state.save(&state_path).unwrap();
559
560 assert!(state_path.exists());
561
562 let loaded = State::load(&state_path).unwrap();
563 assert_eq!(loaded.id, "test-container");
564 assert_eq!(loaded.status, Status::Creating);
565 }
566
567 #[test]
568 fn test_container_state_file_operations() {
569 let dir = tempfile::tempdir().unwrap();
570 let state_path = dir.path().join("container_state.json");
571
572 let mut state = ContainerState::new(
573 "test".to_string(),
574 PathBuf::from("/bundle"),
575 PathBuf::from("/rootfs"),
576 );
577 state.name = Some("my-container".to_string());
578 state.image = Some("alpine:latest".to_string());
579
580 state.save(&state_path).unwrap();
581 assert!(state_path.exists());
582
583 let loaded = ContainerState::load(&state_path).unwrap();
584 assert_eq!(loaded.id(), "test");
585 assert_eq!(loaded.name, Some("my-container".to_string()));
586 assert_eq!(loaded.image, Some("alpine:latest".to_string()));
587 }
588
589 #[test]
590 fn test_container_state_accessors() {
591 let state = ContainerState::new(
592 "test-id".to_string(),
593 PathBuf::from("/path/to/bundle"),
594 PathBuf::from("/path/to/rootfs"),
595 );
596
597 assert_eq!(state.id(), "test-id");
598 assert_eq!(state.status(), Status::Creating);
599 assert_eq!(state.bundle(), Path::new("/path/to/bundle"));
600 assert_eq!(state.rootfs, PathBuf::from("/path/to/rootfs"));
601 }
602
603 #[test]
604 fn test_container_state_timestamps() {
605 let mut state = ContainerState::new(
606 "test".to_string(),
607 PathBuf::from("/bundle"),
608 PathBuf::from("/rootfs"),
609 );
610
611 assert!(state.created <= chrono::Utc::now());
613
614 assert!(state.started.is_none());
616 assert!(state.finished.is_none());
617
618 state.mark_created().unwrap();
619 state.mark_started(1234).unwrap();
620 assert!(state.started.is_some());
621 assert!(state.started.unwrap() <= chrono::Utc::now());
622
623 state.mark_stopped(0).unwrap();
624 assert!(state.finished.is_some());
625 assert!(state.finished.unwrap() >= state.started.unwrap());
626 }
627
628 #[test]
629 fn test_container_state_nonzero_exit_code() {
630 let mut state = ContainerState::new(
631 "test".to_string(),
632 PathBuf::from("/bundle"),
633 PathBuf::from("/rootfs"),
634 );
635
636 state.mark_created().unwrap();
637 state.mark_started(1234).unwrap();
638 state.mark_stopped(137).unwrap(); assert_eq!(state.exit_code, Some(137));
641 }
642
643 #[test]
644 fn test_state_store_new() {
645 let dir = tempfile::tempdir().unwrap();
646 let store = StateStore::new(dir.path()).unwrap();
647 assert!(dir.path().exists());
648 drop(store);
649 }
650
651 #[test]
652 fn test_state_store_save_and_load() {
653 let dir = tempfile::tempdir().unwrap();
654 let store = StateStore::new(dir.path()).unwrap();
655
656 let state = ContainerState::new(
657 "container-1".to_string(),
658 PathBuf::from("/bundle"),
659 PathBuf::from("/rootfs"),
660 );
661
662 store.save(&state).unwrap();
663 assert!(store.exists("container-1"));
664
665 let loaded = store.load("container-1").unwrap();
666 assert_eq!(loaded.id(), "container-1");
667 }
668
669 #[test]
670 fn test_state_store_not_found() {
671 let dir = tempfile::tempdir().unwrap();
672 let store = StateStore::new(dir.path()).unwrap();
673
674 let result = store.load("nonexistent");
675 assert!(result.is_err());
676 }
677
678 #[test]
679 fn test_state_store_delete() {
680 let dir = tempfile::tempdir().unwrap();
681 let store = StateStore::new(dir.path()).unwrap();
682
683 let state = ContainerState::new(
684 "to-delete".to_string(),
685 PathBuf::from("/bundle"),
686 PathBuf::from("/rootfs"),
687 );
688
689 store.save(&state).unwrap();
690 assert!(store.exists("to-delete"));
691
692 store.delete("to-delete").unwrap();
693 assert!(!store.exists("to-delete"));
694 }
695
696 #[test]
697 fn test_state_store_delete_nonexistent() {
698 let dir = tempfile::tempdir().unwrap();
699 let store = StateStore::new(dir.path()).unwrap();
700
701 let result = store.delete("nonexistent");
703 assert!(result.is_ok());
704 }
705
706 #[test]
707 fn test_state_store_list() {
708 let dir = tempfile::tempdir().unwrap();
709 let store = StateStore::new(dir.path()).unwrap();
710
711 assert!(store.list().unwrap().is_empty());
713
714 for i in 1..=3 {
716 let state = ContainerState::new(
717 format!("container-{i}"),
718 PathBuf::from("/bundle"),
719 PathBuf::from("/rootfs"),
720 );
721 store.save(&state).unwrap();
722 }
723
724 let ids = store.list().unwrap();
725 assert_eq!(ids.len(), 3);
726 assert!(ids.contains(&"container-1".to_string()));
727 assert!(ids.contains(&"container-2".to_string()));
728 assert!(ids.contains(&"container-3".to_string()));
729 }
730
731 #[test]
732 fn test_state_store_list_states() {
733 let dir = tempfile::tempdir().unwrap();
734 let store = StateStore::new(dir.path()).unwrap();
735
736 for i in 1..=2 {
737 let mut state = ContainerState::new(
738 format!("container-{i}"),
739 PathBuf::from("/bundle"),
740 PathBuf::from("/rootfs"),
741 );
742 state.name = Some(format!("name-{i}"));
743 store.save(&state).unwrap();
744 }
745
746 let states = store.list_states().unwrap();
747 assert_eq!(states.len(), 2);
748 }
749
750 #[test]
751 fn test_state_store_update() {
752 let dir = tempfile::tempdir().unwrap();
753 let store = StateStore::new(dir.path()).unwrap();
754
755 let mut state = ContainerState::new(
756 "updatable".to_string(),
757 PathBuf::from("/bundle"),
758 PathBuf::from("/rootfs"),
759 );
760
761 store.save(&state).unwrap();
762
763 state.mark_created().unwrap();
765 state.mark_started(9999).unwrap();
766 store.save(&state).unwrap();
767
768 let loaded = store.load("updatable").unwrap();
769 assert_eq!(loaded.status(), Status::Running);
770 assert_eq!(loaded.oci_state.pid, Some(9999));
771 }
772
773 #[test]
774 fn test_state_json_roundtrip() {
775 let mut state = State::new("roundtrip-test".to_string(), PathBuf::from("/bundle"));
776 state.pid = Some(12345);
777 state
778 .annotations
779 .insert("test.key".to_string(), "test.value".to_string());
780
781 let json = state.to_json().unwrap();
782 let parsed: State = serde_json::from_str(&json).unwrap();
783
784 assert_eq!(parsed.id, state.id);
785 assert_eq!(parsed.pid, state.pid);
786 assert_eq!(parsed.annotations, state.annotations);
787 }
788
789 #[test]
790 fn test_transition_creating_to_stopped_on_error() {
791 let mut state = State::new("test".to_string(), PathBuf::from("/bundle"));
792 assert!(state.transition_to(Status::Stopped).is_ok());
794 assert_eq!(state.status, Status::Stopped);
795 }
796
797 #[test]
798 fn test_transition_created_to_stopped_without_running() {
799 let mut state = State::new("test".to_string(), PathBuf::from("/bundle"));
800 state.transition_to(Status::Created).unwrap();
801 assert!(state.transition_to(Status::Stopped).is_ok());
803 assert_eq!(state.status, Status::Stopped);
804 }
805}