Skip to main content

opencode_cloud_core/docker/
state.rs

1//! Image state tracking for provenance information
2//!
3//! Tracks where the current Docker image came from (prebuilt or built)
4//! and which registry it was pulled from.
5
6use chrono::Utc;
7use serde::{Deserialize, Serialize};
8use std::path::PathBuf;
9
10/// Image provenance state
11#[derive(Debug, Clone, Serialize, Deserialize)]
12pub struct ImageState {
13    /// Image version (e.g., "1.0.12")
14    pub version: String,
15    /// Source: "prebuilt" or "build"
16    pub source: String,
17    /// Registry if prebuilt: "ghcr.io" or "docker.io", None for build
18    #[serde(skip_serializing_if = "Option::is_none")]
19    pub registry: Option<String>,
20    /// When the image was acquired (ISO8601)
21    pub acquired_at: String,
22}
23
24impl ImageState {
25    /// Create a new ImageState for a prebuilt image
26    pub fn prebuilt(version: &str, registry: &str) -> Self {
27        Self {
28            version: version.to_string(),
29            source: "prebuilt".to_string(),
30            registry: Some(registry.to_string()),
31            acquired_at: Utc::now().to_rfc3339(),
32        }
33    }
34
35    /// Create a new ImageState for a locally built image
36    pub fn built(version: &str) -> Self {
37        Self {
38            version: version.to_string(),
39            source: "build".to_string(),
40            registry: None,
41            acquired_at: Utc::now().to_rfc3339(),
42        }
43    }
44}
45
46/// Get the path to the image state file
47pub fn get_state_path() -> Option<PathBuf> {
48    crate::config::paths::get_data_dir().map(|p| p.join("image-state.json"))
49}
50
51/// Save image state to disk
52pub fn save_state(state: &ImageState) -> anyhow::Result<()> {
53    let path = get_state_path().ok_or_else(|| anyhow::anyhow!("Could not determine state path"))?;
54
55    // Ensure parent directory exists
56    if let Some(parent) = path.parent() {
57        std::fs::create_dir_all(parent)?;
58    }
59
60    let json = serde_json::to_string_pretty(state)?;
61    std::fs::write(&path, json)?;
62    Ok(())
63}
64
65/// Load image state from disk
66pub fn load_state() -> Option<ImageState> {
67    let path = get_state_path()?;
68    let content = std::fs::read_to_string(&path).ok()?;
69    serde_json::from_str(&content).ok()
70}
71
72/// Clear image state (e.g., after image removal)
73pub fn clear_state() -> anyhow::Result<()> {
74    if let Some(path) = get_state_path() {
75        if path.exists() {
76            std::fs::remove_file(&path)?;
77        }
78    }
79    Ok(())
80}
81
82#[cfg(test)]
83mod tests {
84    use super::*;
85
86    #[test]
87    fn test_image_state_prebuilt() {
88        let state = ImageState::prebuilt("1.0.12", "ghcr.io");
89        assert_eq!(state.version, "1.0.12");
90        assert_eq!(state.source, "prebuilt");
91        assert_eq!(state.registry, Some("ghcr.io".to_string()));
92        assert!(!state.acquired_at.is_empty());
93    }
94
95    #[test]
96    fn test_image_state_built() {
97        let state = ImageState::built("1.0.12");
98        assert_eq!(state.version, "1.0.12");
99        assert_eq!(state.source, "build");
100        assert!(state.registry.is_none());
101    }
102
103    #[test]
104    fn test_image_state_serialize_deserialize() {
105        let state = ImageState::prebuilt("1.0.12", "docker.io");
106        let json = serde_json::to_string(&state).unwrap();
107        let parsed: ImageState = serde_json::from_str(&json).unwrap();
108        assert_eq!(state.version, parsed.version);
109        assert_eq!(state.source, parsed.source);
110        assert_eq!(state.registry, parsed.registry);
111    }
112
113    #[test]
114    fn test_get_state_path() {
115        let path = get_state_path();
116        assert!(path.is_some());
117        let p = path.unwrap();
118        assert!(p.to_string_lossy().contains("image-state.json"));
119    }
120}