manta_shared/image_session/
mod.rs1use std::collections::HashMap;
29
30use manta_backend_dispatcher::types::cfs::session::CfsSessionGetResponse;
31use manta_backend_dispatcher::types::ims::Image;
32
33use crate::common::error::MantaError;
34
35pub const META_BASE: &str = "manta.image_session.base";
37pub const META_GROUPS: &str = "manta.image_session.groups";
39pub const META_CONFIGURATION: &str = "manta.image_session.configuration";
41
42#[derive(Debug, Clone, PartialEq, Eq)]
48pub struct ImageSessionMetadata {
49 pub base: String,
51 pub groups: Vec<String>,
53 pub configuration: String,
55}
56
57pub fn apply(
74 cfs: &CfsSessionGetResponse,
75 ims: &mut Image,
76) -> Result<(), MantaError> {
77 if ims.id.is_none() {
78 return Err(MantaError::MissingField("ims.id".into()));
79 }
80
81 let base = cfs
82 .target
83 .as_ref()
84 .and_then(|t| t.image_map.as_ref())
85 .and_then(|m| m.first())
86 .map(|im| im.source_id.clone())
87 .ok_or_else(|| {
88 MantaError::MissingField("cfs.target.image_map[0].source_id".into())
89 })?;
90
91 let groups = cfs
92 .get_target_hsm()
93 .unwrap_or_default();
94
95 let configuration = cfs
96 .get_configuration_name()
97 .ok_or_else(|| MantaError::MissingField("cfs.configuration.name".into()))?;
98
99 let groups_json = serde_json::to_string(&groups)?;
100
101 let metadata = ims.metadata.get_or_insert_with(HashMap::new);
102 metadata.insert(META_BASE.to_string(), base);
103 metadata.insert(META_GROUPS.to_string(), groups_json);
104 metadata.insert(META_CONFIGURATION.to_string(), configuration);
105
106 Ok(())
107}
108
109pub fn read(id: &str, ims: &Image) -> Result<ImageSessionMetadata, MantaError> {
122 check_id(id, ims)?;
123
124 let metadata = ims
125 .metadata
126 .as_ref()
127 .ok_or_else(|| MantaError::MissingField(META_BASE.into()))?;
128
129 let base = metadata
130 .get(META_BASE)
131 .cloned()
132 .ok_or_else(|| MantaError::MissingField(META_BASE.into()))?;
133 let groups_raw = metadata
134 .get(META_GROUPS)
135 .ok_or_else(|| MantaError::MissingField(META_GROUPS.into()))?;
136 let configuration = metadata
137 .get(META_CONFIGURATION)
138 .cloned()
139 .ok_or_else(|| MantaError::MissingField(META_CONFIGURATION.into()))?;
140
141 let groups: Vec<String> = serde_json::from_str(groups_raw)?;
142
143 Ok(ImageSessionMetadata {
144 base,
145 groups,
146 configuration,
147 })
148}
149
150pub fn clear(id: &str, ims: &mut Image) -> Result<(), MantaError> {
159 check_id(id, ims)?;
160
161 if let Some(metadata) = ims.metadata.as_mut() {
162 metadata.remove(META_BASE);
163 metadata.remove(META_GROUPS);
164 metadata.remove(META_CONFIGURATION);
165 }
166
167 Ok(())
168}
169
170fn check_id(id: &str, ims: &Image) -> Result<(), MantaError> {
171 match ims.id.as_deref() {
172 Some(actual) if actual == id => Ok(()),
173 _ => Err(MantaError::NotFound(format!("image id {id}"))),
174 }
175}
176
177#[cfg(test)]
178mod tests {
179 use super::*;
180 use manta_backend_dispatcher::types::cfs::session::{
181 Configuration, Group, ImageMap, Target,
182 };
183
184 fn sample_cfs(
185 config_name: Option<&str>,
186 base_id: Option<&str>,
187 groups: Vec<&str>,
188 ) -> CfsSessionGetResponse {
189 CfsSessionGetResponse {
190 name: "sess".into(),
191 configuration: config_name.map(|n| Configuration {
192 name: Some(n.into()),
193 limit: None,
194 }),
195 ansible: None,
196 target: Some(Target {
197 definition: Some("image".into()),
198 groups: Some(
199 groups
200 .into_iter()
201 .map(|g| Group {
202 name: g.into(),
203 members: vec![],
204 })
205 .collect(),
206 ),
207 image_map: base_id.map(|b| {
208 vec![ImageMap {
209 source_id: b.into(),
210 result_name: "out".into(),
211 }]
212 }),
213 }),
214 status: None,
215 tags: None,
216 debug_on_failure: false,
217 logs: None,
218 }
219 }
220
221 fn sample_image(id: Option<&str>) -> Image {
222 Image {
223 id: id.map(str::to_owned),
224 created: None,
225 name: "img".into(),
226 link: None,
227 arch: None,
228 metadata: None,
229 }
230 }
231
232 #[test]
233 fn apply_then_read_roundtrip() {
234 let cfs = sample_cfs(Some("cfg-1"), Some("base-1"), vec!["g1", "g2"]);
235 let mut ims = sample_image(Some("img-1"));
236
237 apply(&cfs, &mut ims).unwrap();
238 let got = read("img-1", &ims).unwrap();
239
240 assert_eq!(
241 got,
242 ImageSessionMetadata {
243 base: "base-1".into(),
244 groups: vec!["g1".into(), "g2".into()],
245 configuration: "cfg-1".into(),
246 }
247 );
248 }
249
250 #[test]
251 fn apply_twice_overwrites() {
252 let mut ims = sample_image(Some("img-1"));
253 apply(
254 &sample_cfs(Some("cfg-1"), Some("base-1"), vec!["g1"]),
255 &mut ims,
256 )
257 .unwrap();
258 apply(
259 &sample_cfs(Some("cfg-2"), Some("base-2"), vec!["g2", "g3"]),
260 &mut ims,
261 )
262 .unwrap();
263
264 let got = read("img-1", &ims).unwrap();
265 assert_eq!(got.base, "base-2");
266 assert_eq!(got.groups, vec!["g2".to_string(), "g3".to_string()]);
267 assert_eq!(got.configuration, "cfg-2");
268 }
269
270 #[test]
271 fn read_not_found_on_id_mismatch() {
272 let cfs = sample_cfs(Some("c"), Some("b"), vec!["g"]);
273 let mut ims = sample_image(Some("img-1"));
274 apply(&cfs, &mut ims).unwrap();
275
276 let err = read("img-2", &ims).unwrap_err();
277 assert!(matches!(err, MantaError::NotFound(_)), "got {err:?}");
278 }
279
280 #[test]
281 fn read_not_found_when_image_has_no_id() {
282 let cfs = sample_cfs(Some("c"), Some("b"), vec!["g"]);
283 let mut ims = sample_image(Some("img-1"));
284 apply(&cfs, &mut ims).unwrap();
285 ims.id = None;
286
287 let err = read("img-1", &ims).unwrap_err();
288 assert!(matches!(err, MantaError::NotFound(_)), "got {err:?}");
289 }
290
291 #[test]
292 fn read_missing_field_when_key_absent() {
293 let cfs = sample_cfs(Some("c"), Some("b"), vec!["g"]);
294 let mut ims = sample_image(Some("img-1"));
295 apply(&cfs, &mut ims).unwrap();
296 ims.metadata.as_mut().unwrap().remove(META_GROUPS);
297
298 let err = read("img-1", &ims).unwrap_err();
299 match err {
300 MantaError::MissingField(k) => assert_eq!(k, META_GROUPS),
301 other => panic!("expected MissingField, got {other:?}"),
302 }
303 }
304
305 #[test]
306 fn read_serde_error_on_malformed_groups() {
307 let cfs = sample_cfs(Some("c"), Some("b"), vec!["g"]);
308 let mut ims = sample_image(Some("img-1"));
309 apply(&cfs, &mut ims).unwrap();
310 ims
311 .metadata
312 .as_mut()
313 .unwrap()
314 .insert(META_GROUPS.into(), "not json".into());
315
316 let err = read("img-1", &ims).unwrap_err();
317 assert!(matches!(err, MantaError::SerdeError(_)), "got {err:?}");
318 }
319
320 #[test]
321 fn clear_removes_only_image_session_keys() {
322 let cfs = sample_cfs(Some("c"), Some("b"), vec!["g"]);
323 let mut ims = sample_image(Some("img-1"));
324 apply(&cfs, &mut ims).unwrap();
325 ims
326 .metadata
327 .as_mut()
328 .unwrap()
329 .insert("unrelated".into(), "keep-me".into());
330
331 clear("img-1", &mut ims).unwrap();
332
333 let md = ims.metadata.as_ref().unwrap();
334 assert!(!md.contains_key(META_BASE));
335 assert!(!md.contains_key(META_GROUPS));
336 assert!(!md.contains_key(META_CONFIGURATION));
337 assert_eq!(md.get("unrelated").map(String::as_str), Some("keep-me"));
338 }
339
340 #[test]
341 fn clear_not_found_on_id_mismatch() {
342 let mut ims = sample_image(Some("img-1"));
343 let err = clear("img-2", &mut ims).unwrap_err();
344 assert!(matches!(err, MantaError::NotFound(_)), "got {err:?}");
345 }
346
347 #[test]
348 fn groups_roundtrip_empty() {
349 let cfs = sample_cfs(Some("c"), Some("b"), vec![]);
350 let mut ims = sample_image(Some("img-1"));
351 apply(&cfs, &mut ims).unwrap();
352 assert!(read("img-1", &ims).unwrap().groups.is_empty());
353 }
354
355 #[test]
356 fn groups_roundtrip_single() {
357 let cfs = sample_cfs(Some("c"), Some("b"), vec!["only"]);
358 let mut ims = sample_image(Some("img-1"));
359 apply(&cfs, &mut ims).unwrap();
360 assert_eq!(read("img-1", &ims).unwrap().groups, vec!["only".to_string()]);
361 }
362
363 #[test]
364 fn groups_roundtrip_special_chars() {
365 let cfs = sample_cfs(Some("c"), Some("b"), vec!["a,b", "with\"quote"]);
369 let mut ims = sample_image(Some("img-1"));
370 apply(&cfs, &mut ims).unwrap();
371 assert_eq!(
372 read("img-1", &ims).unwrap().groups,
373 vec!["a,b".to_string(), "with\"quote".to_string()]
374 );
375 }
376
377 #[test]
378 fn apply_initializes_none_metadata() {
379 let cfs = sample_cfs(Some("c"), Some("b"), vec!["g"]);
380 let mut ims = sample_image(Some("img-1"));
381 assert!(ims.metadata.is_none());
382
383 apply(&cfs, &mut ims).unwrap();
384
385 let md = ims.metadata.as_ref().expect("metadata should be Some");
386 assert!(md.contains_key(META_BASE));
387 assert!(md.contains_key(META_GROUPS));
388 assert!(md.contains_key(META_CONFIGURATION));
389 }
390
391 #[test]
392 fn apply_missing_field_when_ims_id_none() {
393 let cfs = sample_cfs(Some("c"), Some("b"), vec!["g"]);
394 let mut ims = sample_image(None);
395 let err = apply(&cfs, &mut ims).unwrap_err();
396 match err {
397 MantaError::MissingField(f) => assert_eq!(f, "ims.id"),
398 other => panic!("expected MissingField, got {other:?}"),
399 }
400 }
401
402 #[test]
403 fn apply_missing_field_when_image_map_empty() {
404 let cfs = sample_cfs(Some("c"), None, vec!["g"]);
405 let mut ims = sample_image(Some("img-1"));
406 let err = apply(&cfs, &mut ims).unwrap_err();
407 assert!(matches!(err, MantaError::MissingField(_)), "got {err:?}");
408 }
409
410 #[test]
411 fn apply_missing_field_when_configuration_name_absent() {
412 let cfs = sample_cfs(None, Some("b"), vec!["g"]);
413 let mut ims = sample_image(Some("img-1"));
414 let err = apply(&cfs, &mut ims).unwrap_err();
415 match err {
416 MantaError::MissingField(f) => assert_eq!(f, "cfs.configuration.name"),
417 other => panic!("expected MissingField, got {other:?}"),
418 }
419 }
420}