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