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