tatara_engine/domain/
volume_manager.rs1use anyhow::{Context, Result};
4use std::collections::HashMap;
5use std::path::{Path, PathBuf};
6use tatara_core::domain::volume::{VolumeClaim, VolumeSource, VolumeSpec};
7use tokio::sync::RwLock;
8use tracing::{debug, info};
9use uuid::Uuid;
10
11#[derive(Debug, Clone)]
13pub struct VolumeHandle {
14 pub name: String,
15 pub path: PathBuf,
16 pub read_only: bool,
17}
18
19pub struct VolumeManager {
21 volume_dir: PathBuf,
23 active: RwLock<HashMap<String, VolumeHandle>>,
25}
26
27impl VolumeManager {
28 pub fn new(volume_dir: PathBuf) -> Self {
29 Self {
30 volume_dir,
31 active: RwLock::new(HashMap::new()),
32 }
33 }
34
35 pub async fn create(&self, spec: &VolumeSpec) -> Result<VolumeHandle> {
37 let path = match &spec.source {
38 VolumeSource::Local { size_mb: _ } => {
39 let vol_path = self.volume_dir.join(&spec.name);
40 tokio::fs::create_dir_all(&vol_path)
41 .await
42 .with_context(|| {
43 format!("failed to create volume dir: {}", vol_path.display())
44 })?;
45 info!(volume = %spec.name, path = %vol_path.display(), "created local volume");
46 vol_path
47 }
48 VolumeSource::HostPath { path } => {
49 let host_path = PathBuf::from(path);
50 if !host_path.exists() {
51 anyhow::bail!("host path does not exist: {}", host_path.display());
52 }
53 debug!(volume = %spec.name, path = %host_path.display(), "using host path volume");
54 host_path
55 }
56 VolumeSource::Nfs { server, path } => {
57 anyhow::bail!(
60 "NFS mounts not yet implemented (volume '{}', server '{}', path '{}'). \
61 Use Local or HostPath volumes, or mount NFS externally and use HostPath.",
62 spec.name,
63 server,
64 path
65 );
66 }
67 };
68
69 let handle = VolumeHandle {
70 name: spec.name.clone(),
71 path,
72 read_only: spec.read_only,
73 };
74
75 self.active
76 .write()
77 .await
78 .insert(spec.name.clone(), handle.clone());
79
80 Ok(handle)
81 }
82
83 pub async fn resolve_mounts(&self, claims: &[VolumeClaim]) -> Result<HashMap<String, String>> {
86 let active = self.active.read().await;
87 let mut mounts = HashMap::new();
88
89 for claim in claims {
90 let handle = active
91 .get(&claim.volume_name)
92 .with_context(|| format!("volume '{}' not found", claim.volume_name))?;
93 mounts.insert(
94 handle.path.to_string_lossy().to_string(),
95 claim.mount_path.clone(),
96 );
97 }
98
99 Ok(mounts)
100 }
101
102 pub async fn release(&self, name: &str) {
104 self.active.write().await.remove(name);
105 debug!(volume = name, "released volume");
106 }
107
108 pub async fn delete(&self, name: &str) -> Result<()> {
110 let path = self.volume_dir.join(name);
111 if path.exists() {
112 tokio::fs::remove_dir_all(&path)
113 .await
114 .with_context(|| format!("failed to delete volume: {}", path.display()))?;
115 info!(volume = name, "deleted local volume");
116 }
117 self.active.write().await.remove(name);
118 Ok(())
119 }
120
121 pub async fn get(&self, name: &str) -> Option<VolumeHandle> {
123 self.active.read().await.get(name).cloned()
124 }
125}
126
127#[cfg(test)]
128mod tests {
129 use super::*;
130 use tempfile::TempDir;
131
132 #[tokio::test]
133 async fn test_create_local_volume() {
134 let tmp = TempDir::new().unwrap();
135 let mgr = VolumeManager::new(tmp.path().to_path_buf());
136
137 let spec = VolumeSpec {
138 name: "data".to_string(),
139 source: VolumeSource::Local { size_mb: None },
140 read_only: false,
141 };
142
143 let handle = mgr.create(&spec).await.unwrap();
144 assert!(handle.path.exists());
145 assert_eq!(handle.name, "data");
146 }
147
148 #[tokio::test]
149 async fn test_resolve_mounts() {
150 let tmp = TempDir::new().unwrap();
151 let mgr = VolumeManager::new(tmp.path().to_path_buf());
152
153 let spec = VolumeSpec {
154 name: "data".to_string(),
155 source: VolumeSource::Local { size_mb: None },
156 read_only: false,
157 };
158 mgr.create(&spec).await.unwrap();
159
160 let claims = vec![VolumeClaim {
161 volume_name: "data".to_string(),
162 mount_path: "/app/data".to_string(),
163 read_only: false,
164 }];
165
166 let mounts = mgr.resolve_mounts(&claims).await.unwrap();
167 assert_eq!(mounts.len(), 1);
168 assert!(mounts.values().next().unwrap() == "/app/data");
169 }
170
171 #[tokio::test]
172 async fn test_delete_volume() {
173 let tmp = TempDir::new().unwrap();
174 let mgr = VolumeManager::new(tmp.path().to_path_buf());
175
176 let spec = VolumeSpec {
177 name: "ephemeral".to_string(),
178 source: VolumeSource::Local { size_mb: None },
179 read_only: false,
180 };
181 let handle = mgr.create(&spec).await.unwrap();
182 assert!(handle.path.exists());
183
184 mgr.delete("ephemeral").await.unwrap();
185 assert!(!handle.path.exists());
186 }
187}