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