1#![warn(missing_docs)]
2
3use std::collections::HashMap;
8use std::default::Default;
9use std::fs;
10use std::path::Path;
11use std::time::SystemTime;
12
13use docker_types::{
14 ConfigInfo, DockerConfig, EndpointConfig, EndpointInfo, EndpointStatus, EndpointType,
15 LogConfig, ResourceLimits, DockerError, SecretInfo,
16};
17
18pub type Result<T> = std::result::Result<T, DockerError>;
20
21pub struct ConfigManager {
23 config_path: String,
25 configs_dir: String,
27 secrets_dir: String,
29 endpoints_dir: String,
31}
32
33impl ConfigManager {
34 pub fn new() -> Result<Self> {
36 let config_path = Self::get_config_path()?;
38 let data_dir = Self::get_default_data_dir();
39 let configs_dir = format!("{}/configs", data_dir);
40 let secrets_dir = format!("{}/secrets", data_dir);
41 let endpoints_dir = format!("{}/endpoints", data_dir);
42
43 fs::create_dir_all(&configs_dir).map_err(|e| DockerError::io_error("create_dir_all", e.to_string()))?;
45 fs::create_dir_all(&secrets_dir).map_err(|e| DockerError::io_error("create_dir_all", e.to_string()))?;
46 fs::create_dir_all(&endpoints_dir).map_err(|e| DockerError::io_error("create_dir_all", e.to_string()))?;
47
48 Ok(Self {
49 config_path,
50 configs_dir,
51 secrets_dir,
52 endpoints_dir,
53 })
54 }
55
56 fn get_config_path() -> Result<String> {
58 #[cfg(target_os = "windows")]
60 let default_path = "C:\\ProgramData\\rusty-docker\\config.toml";
61
62 #[cfg(target_os = "linux")]
63 let default_path = "/etc/rusty-docker/config.toml";
64
65 #[cfg(target_os = "macos")]
66 let default_path = "/usr/local/etc/rusty-docker/config.toml";
67
68 if Path::new(default_path).exists() {
70 Ok(default_path.to_string())
71 } else {
72 Self::create_default_config(default_path)?;
74 Ok(default_path.to_string())
75 }
76 }
77
78 fn create_default_config(path: &str) -> Result<()> {
80 if let Some(dir) = Path::new(path).parent() {
82 fs::create_dir_all(dir).map_err(|e| DockerError::io_error("create_dir_all", e.to_string()))?;
83 }
84
85 let default_config = Self::get_default_config();
87 let config_content = toml::to_string(&default_config).map_err(|e| DockerError::parse_error("DockerConfig", e.to_string()))?;
88
89 fs::write(path, config_content).map_err(|e| DockerError::io_error("write", e.to_string()))?;
91 Ok(())
92 }
93
94 fn get_default_config() -> DockerConfig {
96 DockerConfig {
97 data_dir: Self::get_default_data_dir(),
98 image_dir: format!("{}/images", Self::get_default_data_dir()),
99 container_dir: format!("{}/containers", Self::get_default_data_dir()),
100 network_dir: format!("{}/networks", Self::get_default_data_dir()),
101 default_network: "default".to_string(),
102 default_resources: ResourceLimits {
103 cpu_limit: 1.0,
104 memory_limit: 512,
105 storage_limit: 10,
106 network_limit: 10,
107 },
108 log_config: LogConfig {
109 log_level: "info".to_string(),
110 log_file: format!("{}/logs/docker.log", Self::get_default_data_dir()),
111 max_log_size: 100,
112 },
113 }
114 }
115
116 fn get_default_data_dir() -> String {
118 #[cfg(target_os = "windows")]
119 return "C:\\ProgramData\\rusty-docker\\data".to_string();
120
121 #[cfg(target_os = "linux")]
122 return "/var/lib/rusty-docker".to_string();
123
124 #[cfg(target_os = "macos")]
125 return "/usr/local/var/rusty-docker".to_string();
126 }
127
128 pub fn get_config(&self) -> Result<DockerConfig> {
130 let config_content = fs::read_to_string(&self.config_path).map_err(|e| DockerError::io_error("read_to_string", e.to_string()))?;
132
133 let config: DockerConfig = toml::from_str(&config_content).map_err(|e| DockerError::parse_error("DockerConfig", e.to_string()))?;
135
136 Ok(config)
137 }
138
139 pub fn save_config(&self, config: &DockerConfig) -> Result<()> {
141 let config_content = toml::to_string(config).map_err(|e| DockerError::parse_error("DockerConfig", e.to_string()))?;
143
144 fs::write(&self.config_path, config_content).map_err(|e| DockerError::io_error("write", e.to_string()))?;
146 Ok(())
147 }
148
149 fn generate_id(data: &str) -> String {
151 use sha2::{Digest, Sha256};
152 let mut hasher = Sha256::new();
153 hasher.update(data);
154 hasher.update(
155 SystemTime::now()
156 .duration_since(SystemTime::UNIX_EPOCH)
157 .unwrap()
158 .as_secs()
159 .to_string(),
160 );
161 format!("{:x}", hasher.finalize())
162 }
163
164 pub fn create_config(
166 &self,
167 name: &str,
168 data: &str,
169 labels: HashMap<String, String>,
170 ) -> Result<ConfigInfo> {
171 let id = Self::generate_id(&format!("{}{}", name, data));
172 let config_info = ConfigInfo {
173 id: id.clone(),
174 name: name.to_string(),
175 data: data.to_string(),
176 created_at: SystemTime::now(),
177 labels,
178 };
179
180 let config_content = toml::to_string(&config_info).map_err(|e| DockerError::parse_error("ConfigInfo", e.to_string()))?;
182
183 let config_path = format!("{}/{}.toml", self.configs_dir, id);
185 fs::write(&config_path, config_content).map_err(|e| DockerError::io_error("write", e.to_string()))?;
186
187 Ok(config_info)
188 }
189
190 pub fn update_config(
192 &self,
193 config_id: &str,
194 data: &str,
195 labels: HashMap<String, String>,
196 ) -> Result<ConfigInfo> {
197 let config_path = format!("{}/{}.toml", self.configs_dir, config_id);
199 if !Path::new(&config_path).exists() {
200 return Err(DockerError::not_found("config", format!(
201 "Config {} not found",
202 config_id
203 )));
204 }
205
206 let config_content = fs::read_to_string(&config_path).map_err(|e| DockerError::io_error("read_to_string", e.to_string()))?;
208 let mut config_info: ConfigInfo = toml::from_str(&config_content).map_err(|e| DockerError::parse_error("ConfigInfo", e.to_string()))?;
209
210 config_info.data = data.to_string();
212 config_info.labels = labels;
213
214 let updated_content = toml::to_string(&config_info).map_err(|e| DockerError::parse_error("ConfigInfo", e.to_string()))?;
216 fs::write(&config_path, updated_content).map_err(|e| DockerError::io_error("write", e.to_string()))?;
217
218 Ok(config_info)
219 }
220
221 pub fn delete_config(&self, config_id: &str) -> Result<()> {
223 let config_path = format!("{}/{}.toml", self.configs_dir, config_id);
224 if !Path::new(&config_path).exists() {
225 return Err(DockerError::not_found("config", format!(
226 "Config {} not found",
227 config_id
228 )));
229 }
230
231 fs::remove_file(&config_path).map_err(|e| DockerError::io_error("remove_file", e.to_string()))?;
232 Ok(())
233 }
234
235 pub fn get_config_info(&self, config_id: &str) -> Result<ConfigInfo> {
237 let config_path = format!("{}/{}.toml", self.configs_dir, config_id);
238 if !Path::new(&config_path).exists() {
239 return Err(DockerError::not_found("config", format!(
240 "Config {} not found",
241 config_id
242 )));
243 }
244
245 let config_content = fs::read_to_string(&config_path).map_err(|e| DockerError::io_error("read_to_string", e.to_string()))?;
246 let config_info: ConfigInfo = toml::from_str(&config_content).map_err(|e| DockerError::parse_error("ConfigInfo", e.to_string()))?;
247
248 Ok(config_info)
249 }
250
251 pub fn list_configs(&self) -> Result<Vec<ConfigInfo>> {
253 let mut configs = Vec::new();
254
255 for entry in fs::read_dir(&self.configs_dir).map_err(|e| DockerError::io_error("read_dir", e.to_string()))? {
257 let entry = entry.map_err(|e| DockerError::io_error("entry", e.to_string()))?;
258 let path = entry.path();
259
260 if path.is_file() && path.extension().unwrap_or_default() == "toml" {
261 let config_content = fs::read_to_string(&path).map_err(|e| DockerError::io_error("read_to_string", e.to_string()))?;
262 let config_info: ConfigInfo = toml::from_str(&config_content).map_err(|e| DockerError::parse_error("ConfigInfo", e.to_string()))?;
263 configs.push(config_info);
264 }
265 }
266
267 Ok(configs)
268 }
269
270 pub fn create_secret(
272 &self,
273 name: &str,
274 data: &str,
275 labels: HashMap<String, String>,
276 ) -> Result<SecretInfo> {
277 let id = Self::generate_id(&format!("{}{}", name, data));
278 let digest = Self::generate_id(data);
279
280 let secret_info = SecretInfo {
281 id: id.clone(),
282 name: name.to_string(),
283 created_at: SystemTime::now(),
284 labels,
285 digest,
286 };
287
288 let secret_content = toml::to_string(&secret_info).map_err(|e| DockerError::parse_error("SecretInfo", e.to_string()))?;
290
291 let secret_path = format!("{}/{}.toml", self.secrets_dir, id);
293 fs::write(&secret_path, secret_content).map_err(|e| DockerError::io_error("write", e.to_string()))?;
294
295 let data_path = format!("{}/{}.data", self.secrets_dir, id);
297 fs::write(&data_path, data).map_err(|e| DockerError::io_error("write", e.to_string()))?;
298
299 Ok(secret_info)
300 }
301
302 pub fn delete_secret(&self, secret_id: &str) -> Result<()> {
304 let secret_path = format!("{}/{}.toml", self.secrets_dir, secret_id);
305 let data_path = format!("{}/{}.data", self.secrets_dir, secret_id);
306
307 if !Path::new(&secret_path).exists() {
308 return Err(DockerError::not_found("secret", format!(
309 "Secret {} not found",
310 secret_id
311 )));
312 }
313
314 fs::remove_file(&secret_path).map_err(|e| DockerError::io_error("remove_file", e.to_string()))?;
316 if Path::new(&data_path).exists() {
317 fs::remove_file(&data_path).map_err(|e| DockerError::io_error("remove_file", e.to_string()))?;
318 }
319
320 Ok(())
321 }
322
323 pub fn get_secret_info(&self, secret_id: &str) -> Result<SecretInfo> {
325 let secret_path = format!("{}/{}.toml", self.secrets_dir, secret_id);
326 if !Path::new(&secret_path).exists() {
327 return Err(DockerError::not_found("secret", format!(
328 "Secret {} not found",
329 secret_id
330 )));
331 }
332
333 let secret_content = fs::read_to_string(&secret_path).map_err(|e| DockerError::io_error("read_to_string", e.to_string()))?;
334 let secret_info: SecretInfo = toml::from_str(&secret_content).map_err(|e| DockerError::parse_error("SecretInfo", e.to_string()))?;
335
336 Ok(secret_info)
337 }
338
339 pub fn list_secrets(&self) -> Result<Vec<SecretInfo>> {
341 let mut secrets = Vec::new();
342
343 for entry in fs::read_dir(&self.secrets_dir).map_err(|e| DockerError::io_error("read_dir", e.to_string()))? {
345 let entry = entry.map_err(|e| DockerError::io_error("entry", e.to_string()))?;
346 let path = entry.path();
347
348 if path.is_file() && path.extension().unwrap_or_default() == "toml" {
349 let secret_content = fs::read_to_string(&path).map_err(|e| DockerError::io_error("read_to_string", e.to_string()))?;
350 let secret_info: SecretInfo = toml::from_str(&secret_content).map_err(|e| DockerError::parse_error("SecretInfo", e.to_string()))?;
351 secrets.push(secret_info);
352 }
353 }
354
355 Ok(secrets)
356 }
357
358 pub fn create_endpoint(
360 &self,
361 name: &str,
362 endpoint_type: EndpointType,
363 url: &str,
364 use_tls: bool,
365 tls_cert_path: Option<String>,
366 tls_key_path: Option<String>,
367 tls_ca_path: Option<String>,
368 auth_token: Option<String>,
369 labels: HashMap<String, String>,
370 ) -> Result<EndpointInfo> {
371 let endpoint_type_str = match endpoint_type {
372 EndpointType::Local => "local",
373 EndpointType::Remote => "remote",
374 EndpointType::Cloud => "cloud",
375 };
376 let id = Self::generate_id(&format!("{}{}{}", name, url, endpoint_type_str));
377 let config = EndpointConfig {
378 id: id.clone(),
379 name: name.to_string(),
380 endpoint_type,
381 url: url.to_string(),
382 use_tls,
383 tls_cert_path,
384 tls_key_path,
385 tls_ca_path,
386 auth_token,
387 labels,
388 };
389
390 let endpoint_info = EndpointInfo {
391 config,
392 status: EndpointStatus::Disconnected,
393 created_at: SystemTime::now(),
394 last_connected_at: None,
395 connection_info: None,
396 };
397
398 let endpoint_content = toml::to_string(&endpoint_info).map_err(|e| DockerError::parse_error("EndpointInfo", e.to_string()))?;
400
401 let endpoint_path = format!("{}/{}.toml", self.endpoints_dir, id);
403 fs::write(&endpoint_path, endpoint_content).map_err(|e| DockerError::io_error("write", e.to_string()))?;
404
405 Ok(endpoint_info)
406 }
407
408 pub fn update_endpoint(
410 &self,
411 endpoint_id: &str,
412 name: &str,
413 url: &str,
414 use_tls: bool,
415 tls_cert_path: Option<String>,
416 tls_key_path: Option<String>,
417 tls_ca_path: Option<String>,
418 auth_token: Option<String>,
419 labels: HashMap<String, String>,
420 ) -> Result<EndpointInfo> {
421 let endpoint_path = format!("{}/{}.toml", self.endpoints_dir, endpoint_id);
423 if !Path::new(&endpoint_path).exists() {
424 return Err(DockerError::not_found("endpoint", format!(
425 "Endpoint {} not found",
426 endpoint_id
427 )));
428 }
429
430 let endpoint_content = fs::read_to_string(&endpoint_path).map_err(|e| DockerError::io_error("read_to_string", e.to_string()))?;
432 let mut endpoint_info: EndpointInfo = toml::from_str(&endpoint_content).map_err(|e| DockerError::parse_error("EndpointInfo", e.to_string()))?;
433
434 endpoint_info.config.name = name.to_string();
436 endpoint_info.config.url = url.to_string();
437 endpoint_info.config.use_tls = use_tls;
438 endpoint_info.config.tls_cert_path = tls_cert_path;
439 endpoint_info.config.tls_key_path = tls_key_path;
440 endpoint_info.config.tls_ca_path = tls_ca_path;
441 endpoint_info.config.auth_token = auth_token;
442 endpoint_info.config.labels = labels;
443
444 let updated_content = toml::to_string(&endpoint_info).map_err(|e| DockerError::parse_error("EndpointInfo", e.to_string()))?;
446 fs::write(&endpoint_path, updated_content).map_err(|e| DockerError::io_error("write", e.to_string()))?;
447
448 Ok(endpoint_info)
449 }
450
451 pub fn delete_endpoint(&self, endpoint_id: &str) -> Result<()> {
453 let endpoint_path = format!("{}/{}.toml", self.endpoints_dir, endpoint_id);
454 if !Path::new(&endpoint_path).exists() {
455 return Err(DockerError::not_found("endpoint", format!(
456 "Endpoint {} not found",
457 endpoint_id
458 )));
459 }
460
461 fs::remove_file(&endpoint_path).map_err(|e| DockerError::io_error("remove_file", e.to_string()))?;
462 Ok(())
463 }
464
465 pub fn get_endpoint_info(&self, endpoint_id: &str) -> Result<EndpointInfo> {
467 let endpoint_path = format!("{}/{}.toml", self.endpoints_dir, endpoint_id);
468 if !Path::new(&endpoint_path).exists() {
469 return Err(DockerError::not_found("endpoint", format!(
470 "Endpoint {} not found",
471 endpoint_id
472 )));
473 }
474
475 let endpoint_content = fs::read_to_string(&endpoint_path).map_err(|e| DockerError::io_error("read_to_string", e.to_string()))?;
476 let endpoint_info: EndpointInfo = toml::from_str(&endpoint_content).map_err(|e| DockerError::parse_error("EndpointInfo", e.to_string()))?;
477
478 Ok(endpoint_info)
479 }
480
481 pub fn list_endpoints(&self) -> Result<Vec<EndpointInfo>> {
483 let mut endpoints = Vec::new();
484
485 for entry in fs::read_dir(&self.endpoints_dir).map_err(|e| DockerError::io_error("read_dir", e.to_string()))? {
487 let entry = entry.map_err(|e| DockerError::io_error("entry", e.to_string()))?;
488 let path = entry.path();
489
490 if path.is_file() && path.extension().unwrap_or_default() == "toml" {
491 let endpoint_content = fs::read_to_string(&path).map_err(|e| DockerError::io_error("read_to_string", e.to_string()))?;
492 let endpoint_info: EndpointInfo = toml::from_str(&endpoint_content).map_err(|e| DockerError::parse_error("EndpointInfo", e.to_string()))?;
493 endpoints.push(endpoint_info);
494 }
495 }
496
497 Ok(endpoints)
498 }
499
500 pub fn test_endpoint_connection(&self, endpoint_id: &str) -> Result<EndpointStatus> {
502 Ok(EndpointStatus::Connected)
505 }
506}
507
508impl Default for ConfigManager {
509 fn default() -> Self {
510 Self::new().expect("Failed to create config manager")
511 }
512}