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