lmrc_kubernetes/configmap/
spec.rs

1//! ConfigMap specification builder.
2
3use std::collections::HashMap;
4
5/// Specification for creating a Kubernetes ConfigMap.
6///
7/// ConfigMaps store configuration data as key-value pairs that can be consumed
8/// by pods through environment variables, command-line arguments, or configuration files.
9///
10/// # Examples
11///
12/// ```no_run
13/// use lmrc_kubernetes::ConfigMapSpec;
14///
15/// let configmap = ConfigMapSpec::new("app-config")
16///     .with_data("database.url", "postgres://localhost:5432/mydb")
17///     .with_data("log.level", "info")
18///     .with_label("app", "my-application");
19/// ```
20#[derive(Debug, Clone)]
21pub struct ConfigMapSpec {
22    name: String,
23    data: HashMap<String, String>,
24    binary_data: HashMap<String, Vec<u8>>,
25    labels: HashMap<String, String>,
26    immutable: bool,
27}
28
29impl ConfigMapSpec {
30    /// Creates a new ConfigMapSpec with the given name.
31    ///
32    /// # Arguments
33    ///
34    /// * `name` - The name of the ConfigMap
35    ///
36    /// # Examples
37    ///
38    /// ```
39    /// use lmrc_kubernetes::ConfigMapSpec;
40    ///
41    /// let configmap = ConfigMapSpec::new("my-config");
42    /// ```
43    pub fn new(name: impl Into<String>) -> Self {
44        Self {
45            name: name.into(),
46            data: HashMap::new(),
47            binary_data: HashMap::new(),
48            labels: HashMap::new(),
49            immutable: false,
50        }
51    }
52
53    /// Adds a text data entry to the ConfigMap.
54    ///
55    /// # Arguments
56    ///
57    /// * `key` - The key for the data entry
58    /// * `value` - The string value
59    ///
60    /// # Examples
61    ///
62    /// ```
63    /// use lmrc_kubernetes::ConfigMapSpec;
64    ///
65    /// let configmap = ConfigMapSpec::new("my-config")
66    ///     .with_data("app.name", "my-application");
67    /// ```
68    pub fn with_data(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
69        self.data.insert(key.into(), value.into());
70        self
71    }
72
73    /// Adds multiple text data entries from a HashMap.
74    ///
75    /// # Arguments
76    ///
77    /// * `data` - HashMap of key-value pairs
78    ///
79    /// # Examples
80    ///
81    /// ```
82    /// use lmrc_kubernetes::ConfigMapSpec;
83    /// use std::collections::HashMap;
84    ///
85    /// let mut data = HashMap::new();
86    /// data.insert("key1".to_string(), "value1".to_string());
87    /// data.insert("key2".to_string(), "value2".to_string());
88    ///
89    /// let configmap = ConfigMapSpec::new("my-config")
90    ///     .with_data_map(data);
91    /// ```
92    pub fn with_data_map(mut self, data: HashMap<String, String>) -> Self {
93        self.data.extend(data);
94        self
95    }
96
97    /// Adds binary data to the ConfigMap.
98    ///
99    /// # Arguments
100    ///
101    /// * `key` - The key for the binary data entry
102    /// * `value` - The binary data as bytes
103    ///
104    /// # Examples
105    ///
106    /// ```
107    /// use lmrc_kubernetes::ConfigMapSpec;
108    ///
109    /// let binary_data = vec![0x89, 0x50, 0x4E, 0x47];
110    /// let configmap = ConfigMapSpec::new("my-config")
111    ///     .with_binary_data("image.png", binary_data);
112    /// ```
113    pub fn with_binary_data(mut self, key: impl Into<String>, value: Vec<u8>) -> Self {
114        self.binary_data.insert(key.into(), value);
115        self
116    }
117
118    /// Adds a label to the ConfigMap.
119    ///
120    /// # Arguments
121    ///
122    /// * `key` - The label key
123    /// * `value` - The label value
124    ///
125    /// # Examples
126    ///
127    /// ```
128    /// use lmrc_kubernetes::ConfigMapSpec;
129    ///
130    /// let configmap = ConfigMapSpec::new("my-config")
131    ///     .with_label("environment", "production");
132    /// ```
133    pub fn with_label(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
134        self.labels.insert(key.into(), value.into());
135        self
136    }
137
138    /// Makes the ConfigMap immutable.
139    ///
140    /// Once a ConfigMap is marked as immutable, its data cannot be changed.
141    /// This provides protection against accidental updates and improves performance.
142    ///
143    /// # Examples
144    ///
145    /// ```
146    /// use lmrc_kubernetes::ConfigMapSpec;
147    ///
148    /// let configmap = ConfigMapSpec::new("my-config")
149    ///     .with_data("version", "1.0.0")
150    ///     .immutable(true);
151    /// ```
152    pub fn immutable(mut self, immutable: bool) -> Self {
153        self.immutable = immutable;
154        self
155    }
156
157    /// Loads data from a file content.
158    ///
159    /// # Arguments
160    ///
161    /// * `filename` - The key to use (typically the filename)
162    /// * `content` - The file content
163    ///
164    /// # Examples
165    ///
166    /// ```
167    /// use lmrc_kubernetes::ConfigMapSpec;
168    ///
169    /// let config_content = "database:\n  host: localhost\n  port: 5432";
170    /// let configmap = ConfigMapSpec::new("my-config")
171    ///     .from_file("database.yaml", config_content);
172    /// ```
173    pub fn from_file(mut self, filename: impl Into<String>, content: impl Into<String>) -> Self {
174        self.data.insert(filename.into(), content.into());
175        self
176    }
177
178    /// Validates the ConfigMap specification.
179    ///
180    /// # Returns
181    ///
182    /// Returns `Ok(())` if valid, or an error message if invalid.
183    ///
184    /// # Examples
185    ///
186    /// ```
187    /// use lmrc_kubernetes::ConfigMapSpec;
188    ///
189    /// let configmap = ConfigMapSpec::new("my-config")
190    ///     .with_data("key", "value");
191    ///
192    /// assert!(configmap.validate().is_ok());
193    /// ```
194    pub fn validate(&self) -> Result<(), String> {
195        if self.name.is_empty() {
196            return Err("ConfigMap name cannot be empty".to_string());
197        }
198
199        if self.data.is_empty() && self.binary_data.is_empty() {
200            return Err("ConfigMap must have at least one data or binary_data entry".to_string());
201        }
202
203        // Validate ConfigMap name follows DNS subdomain rules
204        if !self
205            .name
206            .chars()
207            .all(|c| c.is_ascii_alphanumeric() || c == '-' || c == '.')
208        {
209            return Err(
210                "ConfigMap name must contain only alphanumeric characters, '-', or '.'".to_string(),
211            );
212        }
213
214        if self.name.starts_with('-') || self.name.ends_with('-') {
215            return Err("ConfigMap name cannot start or end with '-'".to_string());
216        }
217
218        Ok(())
219    }
220
221    // Getters
222
223    /// Returns the ConfigMap name.
224    pub fn name(&self) -> &str {
225        &self.name
226    }
227
228    /// Returns the data entries.
229    pub fn data(&self) -> &HashMap<String, String> {
230        &self.data
231    }
232
233    /// Returns the binary data entries.
234    pub fn binary_data(&self) -> &HashMap<String, Vec<u8>> {
235        &self.binary_data
236    }
237
238    /// Returns the labels.
239    pub fn labels(&self) -> &HashMap<String, String> {
240        &self.labels
241    }
242
243    /// Returns whether the ConfigMap is immutable.
244    pub fn is_immutable(&self) -> bool {
245        self.immutable
246    }
247}
248
249#[cfg(test)]
250mod tests {
251    use super::*;
252
253    #[test]
254    fn test_new_configmap() {
255        let cm = ConfigMapSpec::new("test-config");
256        assert_eq!(cm.name(), "test-config");
257        assert!(cm.data().is_empty());
258    }
259
260    #[test]
261    fn test_with_data() {
262        let cm = ConfigMapSpec::new("test-config")
263            .with_data("key1", "value1")
264            .with_data("key2", "value2");
265
266        assert_eq!(cm.data().len(), 2);
267        assert_eq!(cm.data().get("key1").unwrap(), "value1");
268    }
269
270    #[test]
271    fn test_with_binary_data() {
272        let cm = ConfigMapSpec::new("test-config").with_binary_data("file.bin", vec![1, 2, 3, 4]);
273
274        assert_eq!(cm.binary_data().len(), 1);
275        assert_eq!(cm.binary_data().get("file.bin").unwrap(), &vec![1, 2, 3, 4]);
276    }
277
278    #[test]
279    fn test_immutable() {
280        let cm = ConfigMapSpec::new("test-config")
281            .with_data("key", "value")
282            .immutable(true);
283
284        assert!(cm.is_immutable());
285    }
286
287    #[test]
288    fn test_validation_empty_name() {
289        let cm = ConfigMapSpec::new("");
290        assert!(cm.validate().is_err());
291    }
292
293    #[test]
294    fn test_validation_no_data() {
295        let cm = ConfigMapSpec::new("test");
296        assert!(cm.validate().is_err());
297    }
298
299    #[test]
300    fn test_validation_valid() {
301        let cm = ConfigMapSpec::new("test-config").with_data("key", "value");
302        assert!(cm.validate().is_ok());
303    }
304}