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_SSH: &str = "opencode-ssh";
33
34pub 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
45pub const MOUNT_SESSION: &str = "/home/opencoder/.local/share/opencode";
47
48pub const MOUNT_STATE: &str = "/home/opencoder/.local/state/opencode";
50
51pub const MOUNT_CACHE: &str = "/home/opencoder/.cache/opencode";
53
54pub const MOUNT_PROJECTS: &str = "/home/opencoder/workspace";
56
57pub const MOUNT_CONFIG: &str = "/home/opencoder/.config/opencode";
59
60pub const MOUNT_USERS: &str = "/var/lib/opencode-users";
62
63pub const MOUNT_SSH: &str = "/home/opencoder/.ssh";
65
66pub 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
81async 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 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 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 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
116pub 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
131pub 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
148pub 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 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}