opencode_cloud_core/docker/
volume.rs1use 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
13pub const VOLUME_SESSION: &str = "opencode-data";
15
16pub const VOLUME_STATE: &str = "opencode-state";
18
19pub const VOLUME_CACHE: &str = "opencode-cache";
21
22pub const VOLUME_PROJECTS: &str = "opencode-workspace";
24
25pub const VOLUME_CONFIG: &str = "opencode-config";
27
28pub const VOLUME_USERS: &str = "opencode-users";
30
31pub const VOLUME_NAMES: [&str; 6] = [
33 VOLUME_SESSION,
34 VOLUME_STATE,
35 VOLUME_CACHE,
36 VOLUME_PROJECTS,
37 VOLUME_CONFIG,
38 VOLUME_USERS,
39];
40
41pub const MOUNT_SESSION: &str = "/home/opencoder/.local/share/opencode";
43
44pub const MOUNT_STATE: &str = "/home/opencoder/.local/state/opencode";
46
47pub const MOUNT_CACHE: &str = "/home/opencoder/.cache/opencode";
49
50pub const MOUNT_PROJECTS: &str = "/home/opencoder/workspace";
52
53pub const MOUNT_CONFIG: &str = "/home/opencoder/.config/opencode";
55
56pub const MOUNT_USERS: &str = "/var/lib/opencode-users";
58
59pub async fn ensure_volumes_exist(client: &DockerClient) -> Result<(), DockerError> {
64 debug!("Ensuring all required volumes exist");
65 let names = active_resource_names();
66 for volume_name in names.volume_names() {
67 ensure_volume_exists(client, volume_name, names.instance_id.as_deref()).await?;
68 }
69
70 debug!("All volumes verified/created");
71 Ok(())
72}
73
74async fn ensure_volume_exists(
76 client: &DockerClient,
77 name: &str,
78 instance_id: Option<&str>,
79) -> Result<(), DockerError> {
80 debug!("Checking volume: {}", name);
81
82 let mut labels = HashMap::from([("managed-by".to_string(), "opencode-cloud".to_string())]);
85 if let Some(instance_id) = instance_id {
86 labels.insert(INSTANCE_LABEL_KEY.to_string(), instance_id.to_string());
87 }
88
89 let options = VolumeCreateRequest {
91 name: Some(name.to_string()),
92 driver: Some("local".to_string()),
93 driver_opts: Some(HashMap::new()),
94 labels: Some(labels),
95 cluster_volume_spec: None,
96 };
97
98 client
100 .inner()
101 .create_volume(options)
102 .await
103 .map_err(|e| DockerError::Volume(format!("Failed to create volume {name}: {e}")))?;
104
105 debug!("Volume {} ready", name);
106 Ok(())
107}
108
109pub async fn volume_exists(client: &DockerClient, name: &str) -> Result<bool, DockerError> {
111 debug!("Checking if volume exists: {}", name);
112
113 match client.inner().inspect_volume(name).await {
114 Ok(_) => Ok(true),
115 Err(bollard::errors::Error::DockerResponseServerError {
116 status_code: 404, ..
117 }) => Ok(false),
118 Err(e) => Err(DockerError::Volume(format!(
119 "Failed to inspect volume {name}: {e}"
120 ))),
121 }
122}
123
124pub async fn remove_volume(client: &DockerClient, name: &str) -> Result<(), DockerError> {
129 debug!("Removing volume: {}", name);
130
131 client
132 .inner()
133 .remove_volume(name, None::<RemoveVolumeOptions>)
134 .await
135 .map_err(|e| DockerError::Volume(format!("Failed to remove volume {name}: {e}")))?;
136
137 debug!("Volume {} removed", name);
138 Ok(())
139}
140
141pub async fn remove_all_volumes(client: &DockerClient) -> Result<(), DockerError> {
145 debug!("Removing all opencode-cloud volumes");
146 let names = active_resource_names();
147
148 for volume_name in names.volume_names() {
149 if volume_exists(client, volume_name).await? {
151 remove_volume(client, volume_name).await?;
152 }
153 }
154
155 debug!("All volumes removed");
156 Ok(())
157}
158
159#[cfg(test)]
160mod tests {
161 use super::*;
162
163 #[test]
164 fn volume_constants_are_correct() {
165 assert_eq!(VOLUME_SESSION, "opencode-data");
166 assert_eq!(VOLUME_STATE, "opencode-state");
167 assert_eq!(VOLUME_CACHE, "opencode-cache");
168 assert_eq!(VOLUME_PROJECTS, "opencode-workspace");
169 assert_eq!(VOLUME_CONFIG, "opencode-config");
170 assert_eq!(VOLUME_USERS, "opencode-users");
171 }
172
173 #[test]
174 fn volume_names_array_has_all_volumes() {
175 assert_eq!(VOLUME_NAMES.len(), 6);
176 assert!(VOLUME_NAMES.contains(&VOLUME_SESSION));
177 assert!(VOLUME_NAMES.contains(&VOLUME_STATE));
178 assert!(VOLUME_NAMES.contains(&VOLUME_CACHE));
179 assert!(VOLUME_NAMES.contains(&VOLUME_PROJECTS));
180 assert!(VOLUME_NAMES.contains(&VOLUME_CONFIG));
181 assert!(VOLUME_NAMES.contains(&VOLUME_USERS));
182 }
183
184 #[test]
185 fn mount_points_are_correct() {
186 assert_eq!(MOUNT_SESSION, "/home/opencoder/.local/share/opencode");
187 assert_eq!(MOUNT_STATE, "/home/opencoder/.local/state/opencode");
188 assert_eq!(MOUNT_CACHE, "/home/opencoder/.cache/opencode");
189 assert_eq!(MOUNT_PROJECTS, "/home/opencoder/workspace");
190 assert_eq!(MOUNT_CONFIG, "/home/opencoder/.config/opencode");
191 assert_eq!(MOUNT_USERS, "/var/lib/opencode-users");
192 }
193}