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}