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