Skip to main content

opencode_cloud_core/docker/
volume.rs

1//! Docker volume management
2//!
3//! This module provides functions to create and manage Docker volumes
4//! for persistent storage across container restarts.
5
6use super::{DockerClient, DockerError};
7use bollard::volume::CreateVolumeOptions;
8use std::collections::HashMap;
9use tracing::debug;
10
11/// Volume name for opencode data
12pub const VOLUME_SESSION: &str = "opencode-data";
13
14/// Volume name for opencode state
15pub const VOLUME_STATE: &str = "opencode-state";
16
17/// Volume name for opencode cache
18pub const VOLUME_CACHE: &str = "opencode-cache";
19
20/// Volume name for project files
21pub const VOLUME_PROJECTS: &str = "opencode-workspace";
22
23/// Volume name for opencode configuration
24pub const VOLUME_CONFIG: &str = "opencode-config";
25
26/// All volume names as array for iteration
27pub const VOLUME_NAMES: [&str; 5] = [
28    VOLUME_SESSION,
29    VOLUME_STATE,
30    VOLUME_CACHE,
31    VOLUME_PROJECTS,
32    VOLUME_CONFIG,
33];
34
35/// Mount point for opencode data inside container
36pub const MOUNT_SESSION: &str = "/home/opencode/.local/share/opencode";
37
38/// Mount point for opencode state inside container
39pub const MOUNT_STATE: &str = "/home/opencode/.local/state/opencode";
40
41/// Mount point for opencode cache inside container
42pub const MOUNT_CACHE: &str = "/home/opencode/.cache/opencode";
43
44/// Mount point for project files inside container
45pub const MOUNT_PROJECTS: &str = "/home/opencode/workspace";
46
47/// Mount point for configuration inside container
48pub const MOUNT_CONFIG: &str = "/home/opencode/.config/opencode";
49
50/// Ensure all required volumes exist
51///
52/// Creates volumes if they don't exist. This operation is idempotent -
53/// calling it multiple times has no additional effect.
54pub async fn ensure_volumes_exist(client: &DockerClient) -> Result<(), DockerError> {
55    debug!("Ensuring all required volumes exist");
56
57    for volume_name in VOLUME_NAMES {
58        ensure_volume_exists(client, volume_name).await?;
59    }
60
61    debug!("All volumes verified/created");
62    Ok(())
63}
64
65/// Ensure a specific volume exists
66async fn ensure_volume_exists(client: &DockerClient, name: &str) -> Result<(), DockerError> {
67    debug!("Checking volume: {}", name);
68
69    // Create volume options with default local driver
70    let options = CreateVolumeOptions {
71        name,
72        driver: "local",
73        driver_opts: HashMap::new(),
74        labels: HashMap::from([("managed-by", "opencode-cloud")]),
75    };
76
77    // create_volume is idempotent - returns existing volume if it exists
78    client
79        .inner()
80        .create_volume(options)
81        .await
82        .map_err(|e| DockerError::Volume(format!("Failed to create volume {name}: {e}")))?;
83
84    debug!("Volume {} ready", name);
85    Ok(())
86}
87
88/// Check if a specific volume exists
89pub async fn volume_exists(client: &DockerClient, name: &str) -> Result<bool, DockerError> {
90    debug!("Checking if volume exists: {}", name);
91
92    match client.inner().inspect_volume(name).await {
93        Ok(_) => Ok(true),
94        Err(bollard::errors::Error::DockerResponseServerError {
95            status_code: 404, ..
96        }) => Ok(false),
97        Err(e) => Err(DockerError::Volume(format!(
98            "Failed to inspect volume {name}: {e}"
99        ))),
100    }
101}
102
103/// Remove a volume
104///
105/// Returns error if volume is in use by a container.
106/// Use force_remove_volume for cleanup during uninstall.
107pub async fn remove_volume(client: &DockerClient, name: &str) -> Result<(), DockerError> {
108    debug!("Removing volume: {}", name);
109
110    client
111        .inner()
112        .remove_volume(name, None)
113        .await
114        .map_err(|e| DockerError::Volume(format!("Failed to remove volume {name}: {e}")))?;
115
116    debug!("Volume {} removed", name);
117    Ok(())
118}
119
120/// Remove all opencode-cloud volumes
121///
122/// Used during uninstall. Fails if any volume is in use.
123pub async fn remove_all_volumes(client: &DockerClient) -> Result<(), DockerError> {
124    debug!("Removing all opencode-cloud volumes");
125
126    for volume_name in VOLUME_NAMES {
127        // Check if volume exists before trying to remove
128        if volume_exists(client, volume_name).await? {
129            remove_volume(client, volume_name).await?;
130        }
131    }
132
133    debug!("All volumes removed");
134    Ok(())
135}
136
137#[cfg(test)]
138mod tests {
139    use super::*;
140
141    #[test]
142    fn volume_constants_are_correct() {
143        assert_eq!(VOLUME_SESSION, "opencode-data");
144        assert_eq!(VOLUME_STATE, "opencode-state");
145        assert_eq!(VOLUME_CACHE, "opencode-cache");
146        assert_eq!(VOLUME_PROJECTS, "opencode-workspace");
147        assert_eq!(VOLUME_CONFIG, "opencode-config");
148    }
149
150    #[test]
151    fn volume_names_array_has_all_volumes() {
152        assert_eq!(VOLUME_NAMES.len(), 5);
153        assert!(VOLUME_NAMES.contains(&VOLUME_SESSION));
154        assert!(VOLUME_NAMES.contains(&VOLUME_STATE));
155        assert!(VOLUME_NAMES.contains(&VOLUME_CACHE));
156        assert!(VOLUME_NAMES.contains(&VOLUME_PROJECTS));
157        assert!(VOLUME_NAMES.contains(&VOLUME_CONFIG));
158    }
159
160    #[test]
161    fn mount_points_are_correct() {
162        assert_eq!(MOUNT_SESSION, "/home/opencode/.local/share/opencode");
163        assert_eq!(MOUNT_STATE, "/home/opencode/.local/state/opencode");
164        assert_eq!(MOUNT_CACHE, "/home/opencode/.cache/opencode");
165        assert_eq!(MOUNT_PROJECTS, "/home/opencode/workspace");
166        assert_eq!(MOUNT_CONFIG, "/home/opencode/.config/opencode");
167    }
168}