docker_image_pusher/
config.rs

1//! Configuration structures and utilities
2//!
3//! This module defines configuration types for authentication and registry settings, including
4//! [`AuthConfig`] for credentials and [`RegistryConfig`] for registry connection details.
5//! It provides helpers for constructing and validating configuration from CLI or environment.
6
7use crate::error::{PusherError, Result};
8use serde::{Deserialize, Serialize};
9
10#[derive(Debug, Clone, Serialize, Deserialize)]
11pub struct AuthConfig {
12    pub username: String,
13    pub password: String,
14}
15
16impl AuthConfig {
17    pub fn new(username: String, password: String) -> Self {
18        Self { username, password }
19    }
20
21    pub fn from_optional(username: Option<String>, password: Option<String>) -> Result<Self> {
22        match (username, password) {
23            (Some(u), Some(p)) => Ok(Self::new(u, p)),
24            (None, _) => Err(PusherError::Config(
25                "Username is required for authentication".to_string(),
26            )),
27            (_, None) => Err(PusherError::Config(
28                "Password is required for authentication".to_string(),
29            )),
30        }
31    }
32}
33
34#[derive(Debug, Clone, Serialize, Deserialize)]
35pub struct RegistryConfig {
36    pub address: String,
37    pub repository: String,
38    pub tag: String,
39    pub auth: Option<AuthConfig>,
40    pub skip_tls: bool,
41    pub timeout: u64,
42}
43
44impl RegistryConfig {
45    pub fn new(address: String, repository: String, tag: String) -> Self {
46        Self {
47            address,
48            repository,
49            tag,
50            auth: None,
51            skip_tls: false,
52            timeout: 7200, // 2 hours default
53        }
54    }
55
56    pub fn with_auth(mut self, auth: AuthConfig) -> Self {
57        self.auth = Some(auth);
58        self
59    }
60
61    pub fn with_skip_tls(mut self, skip_tls: bool) -> Self {
62        self.skip_tls = skip_tls;
63        self
64    }
65
66    pub fn with_timeout(mut self, timeout: u64) -> Self {
67        self.timeout = timeout;
68        self
69    }
70}
71
72#[derive(Debug, Clone, Serialize, Deserialize)]
73pub struct AppConfig {
74    pub registry: RegistryConfig,
75    pub file_path: String,
76    pub large_layer_threshold: u64,
77    pub max_concurrent: usize,
78    pub retry_attempts: usize,
79    pub verbose: bool,
80    pub dry_run: bool,
81}
82
83impl AppConfig {
84    pub fn new(registry_url: &str, repository: &str, tag: &str, file_path: String) -> Result<Self> {
85        let registry_config = RegistryConfig::new(
86            registry_url.to_string(),
87            repository.to_string(),
88            tag.to_string(),
89        );
90
91        Ok(Self {
92            registry: registry_config,
93            file_path,
94            large_layer_threshold: 1024 * 1024 * 1024, // 1GB default
95            max_concurrent: 1,
96            retry_attempts: 3,
97            verbose: false,
98            dry_run: false,
99        })
100    }
101
102    pub fn with_auth(mut self, username: String, password: String) -> Self {
103        let auth = AuthConfig::new(username, password);
104        self.registry = self.registry.with_auth(auth);
105        self
106    }
107
108    pub fn with_optional_auth(
109        mut self,
110        username: Option<String>,
111        password: Option<String>,
112    ) -> Result<Self> {
113        if let (Some(u), Some(p)) = (username, password) {
114            self = self.with_auth(u, p);
115        }
116        Ok(self)
117    }
118
119    pub fn has_auth(&self) -> bool {
120        self.registry.auth.is_some()
121    }
122
123    pub fn with_large_layer_threshold(mut self, threshold: u64) -> Self {
124        self.large_layer_threshold = threshold;
125        self
126    }
127
128    pub fn with_timeout(mut self, timeout: u64) -> Self {
129        self.registry = self.registry.with_timeout(timeout);
130        self
131    }
132
133    pub fn with_skip_tls(mut self, skip_tls: bool) -> Self {
134        self.registry = self.registry.with_skip_tls(skip_tls);
135        self
136    }
137
138    pub fn with_verbose(mut self, verbose: bool) -> Self {
139        self.verbose = verbose;
140        self
141    }
142
143    pub fn with_dry_run(mut self, dry_run: bool) -> Self {
144        self.dry_run = dry_run;
145        self
146    }
147
148    pub fn with_max_concurrent(mut self, max_concurrent: usize) -> Self {
149        self.max_concurrent = max_concurrent;
150        self
151    }
152
153    pub fn with_retry_attempts(mut self, retry_attempts: usize) -> Self {
154        self.retry_attempts = retry_attempts;
155        self
156    }
157
158    pub fn parse_repository_url(url: &str) -> Result<(String, String, String)> {
159        let parsed_url = url::Url::parse(url)
160            .map_err(|e| PusherError::Config(format!("Invalid repository URL: {}", e)))?;
161
162        let registry_address = format!(
163            "{}://{}",
164            parsed_url.scheme(),
165            parsed_url.host_str().unwrap_or("localhost")
166        );
167
168        let path = parsed_url.path().trim_start_matches('/');
169        let (repository, tag) = if let Some(colon_pos) = path.rfind(':') {
170            let (repo, tag_part) = path.split_at(colon_pos);
171            (repo, &tag_part[1..]) // Remove the ':' prefix
172        } else {
173            (path, "latest")
174        };
175
176        Ok((registry_address, repository.to_string(), tag.to_string()))
177    }
178}