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