scl_lib/api_objects/
mod.rs

1// SPDX-License-Identifier: EUPL-1.2
2use regex::Regex;
3use serde::de::DeserializeOwned;
4use serde::{Deserialize, Serialize};
5use std::convert::TryFrom;
6use std::fmt::{Display, Formatter};
7use std::ops::Deref;
8use uuid::Uuid;
9
10#[cfg(feature = "openapi")]
11use aide::OperationIo;
12#[cfg(feature = "openapi")]
13use schemars::JsonSchema;
14
15mod controller;
16mod error;
17mod node;
18mod router;
19mod separation_context;
20mod virtual_machine;
21mod volume;
22
23pub use controller::{Controller, ControllerKind, ControllerStatus};
24pub use error::{Error, Result, TransitionError, ValueSpaceExhaustedError};
25pub use node::{Endpoint, Node, NodeStatus, Resources};
26pub use router::{AssignedDeviceNames, ForwardedPort, Router, RouterStatus};
27pub use separation_context::{AvailableVlanTags, SeparationContext, VlanTag};
28pub use virtual_machine::{
29    BootVolume, LocalStorage, NotAssignedStatus, TargetVmStatus, TransitionInfo, VirtualMachine,
30    VmCondition, VmSpec, VmStatus,
31};
32pub use volume::{derive_volume_file_path, Volume, VolumeStatus};
33
34static SCL_NAME_REGEX: &str = r"^[a-z]([0-9a-z-]*[0-9a-z])?$";
35
36/// Non-empty string consisting of lowercase alphanumeric characters as
37/// well as hyphens `-`. The name must start with a letter and must end
38/// with an alphanumeric character The length must not exceed 63. This
39/// way, `SclNames` can be used as DNS label names as specified in
40/// [RFC 1035](https://datatracker.ietf.org/doc/html/rfc1035#section-2.3.1).
41#[derive(Clone, Debug, Deserialize, Eq, Ord, PartialEq, PartialOrd, Serialize)]
42#[cfg_attr(feature = "openapi", derive(OperationIo, JsonSchema))]
43#[cfg_attr(feature = "openapi", aide(output))]
44#[serde(try_from = "String")]
45pub struct SclName(
46    #[cfg_attr(feature = "openapi", validate(length(min = 1, max = 63)))]
47    #[cfg_attr(feature = "openapi", validate(regex(path = "SCL_NAME_REGEX")))]
48    String,
49);
50
51impl TryFrom<String> for SclName {
52    type Error = Error;
53
54    fn try_from(value: String) -> Result<Self> {
55        if value.is_empty() {
56            return Err(Error::ParsingFailed(
57                "SclName must not be empty".to_string(),
58            ));
59        }
60
61        if value.len() > 63 {
62            return Err(Error::ParsingFailed(
63                "SclName length must not exceed 63".to_string(),
64            ));
65        }
66
67        let re = Regex::new(SCL_NAME_REGEX).unwrap(); // TODO lazy_static?
68        if re.is_match(&value) {
69            Ok(Self(value))
70        } else {
71            Err(Error::ParsingFailed(
72                "SclName contains illegal characters".to_string(),
73            ))
74        }
75    }
76}
77
78impl TryFrom<&str> for SclName {
79    type Error = Error;
80
81    fn try_from(value: &str) -> Result<Self> {
82        SclName::try_from(value.to_string())
83    }
84}
85
86impl Deref for SclName {
87    type Target = String;
88
89    fn deref(&self) -> &Self::Target {
90        &self.0
91    }
92}
93
94impl Display for SclName {
95    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
96        self.0.fmt(f)
97    }
98}
99
100/// This trait is implemented by all SCL API objects to provide uniform access to common properties
101/// of the objects.
102pub trait SclObject:
103    Clone + DeserializeOwned + Eq + Ord + PartialEq + PartialOrd + Serialize
104{
105    /// Constant prefix to identify the objects root.
106    ///
107    /// The prefix is used for both API endpoint and DB key construction and must start with a
108    /// `"/"`.
109    const PREFIX: &'static str;
110
111    fn uuid(&self) -> Uuid {
112        let name = self.name().to_string();
113        let uuid_name = match self.separation_context() {
114            Some(sc) => format!("{sc}{name}"),
115            None => name,
116        };
117        Uuid::new_v5(&Uuid::NAMESPACE_OID, uuid_name.as_bytes())
118    }
119
120    /// Returns the [SclName] of the [SclObject].
121    fn name(&self) -> &SclName;
122
123    /// Returns the `SclName` of the separation context if the SclObject is connected to one,
124    /// otherwise default `None`.
125    fn separation_context(&self) -> Option<&SclName> {
126        None
127    }
128
129    /// Returns the API endpoint for a [SclObject].
130    ///
131    /// # Examples
132    ///
133    /// ```
134    /// use scl_lib::api_objects::{Controller, SclObject, VirtualMachine, Volume};
135    ///
136    /// assert_eq!(VirtualMachine::api_endpoint("sc-01", "vm-01"), "/scs/sc-01/vms/vm-01");
137    /// assert_eq!(Controller::api_endpoint(None, "ctrl-01"), "/controllers/ctrl-01");
138    /// assert_eq!(Volume::api_endpoint("sc-01", None), "/scs/sc-01/volumes");
139    /// ```
140    fn api_endpoint<'a>(
141        sc: impl Into<Option<&'a str>>,
142        name: impl Into<Option<&'a str>>,
143    ) -> String {
144        let ep = match sc.into() {
145            Some(sc) => format!("{}/{}{}", SeparationContext::PREFIX, sc, Self::PREFIX),
146            _ => Self::PREFIX.into(),
147        };
148        match name.into() {
149            Some(name) => format!("{}/{}", ep, name),
150            None => ep,
151        }
152    }
153
154    /// Returns the API endpoint of the [SclObject].
155    ///
156    /// # Examples
157    ///
158    /// ```
159    /// use std::convert::TryFrom;
160    /// use scl_lib::api_objects::{Controller, ControllerKind, SclName, SclObject};
161    ///
162    /// let ctrl = Controller::new(
163    ///     SclName::try_from("ctrl-01".to_string()).unwrap(),
164    ///     ControllerKind::VmController);
165    ///
166    /// assert_eq!(ctrl.get_api_endpoint(), "/controllers/ctrl-01");
167    /// ```
168    fn get_api_endpoint(&self) -> String {
169        Self::api_endpoint(
170            self.separation_context().map(|sc| sc.as_str()),
171            self.name().as_str(),
172        )
173    }
174
175    /// Returns the database key for a [SclObject].
176    ///
177    /// # Examples
178    ///
179    /// ```
180    /// use scl_lib::api_objects::{Controller, SclObject, VirtualMachine, Volume};
181    ///
182    /// assert_eq!(VirtualMachine::db_key("sc-01", "vm-01"), "/vms/sc-01/vm-01");
183    /// assert_eq!(Controller::db_key(None, "ctrl-01"), "/controllers/ctrl-01");
184    /// assert_eq!(Volume::db_key("sc-01", None), "/volumes/sc-01");
185    /// ```
186    fn db_key<'a>(sc: impl Into<Option<&'a str>>, name: impl Into<Option<&'a str>>) -> String {
187        let key = match sc.into() {
188            Some(sc) => format!("{}/{}", Self::PREFIX, sc),
189            _ => Self::PREFIX.into(),
190        };
191        match name.into() {
192            Some(name) => format!("{}/{}", key, name),
193            _ => key,
194        }
195    }
196
197    /// Returns the database key of the [SclObject].
198    ///
199    /// # Examples
200    ///
201    /// ```
202    /// use std::convert::TryFrom;
203    /// use scl_lib::api_objects::{Controller, ControllerKind, SclName, SclObject};
204    ///
205    /// let ctrl = Controller::new(
206    ///     SclName::try_from("ctrl-01".to_string()).unwrap(),
207    ///     ControllerKind::VmController);
208    ///
209    /// assert_eq!(ctrl.get_db_key(), "/controllers/ctrl-01");
210    /// ```
211    fn get_db_key(&self) -> String {
212        Self::db_key(
213            self.separation_context().map(|sc| sc.as_str()),
214            self.name().as_str(),
215        )
216    }
217
218    /// Providing access to `MetaData` is essential for our optimistic concurrency control.
219    fn metadata(&self) -> &MetaData;
220
221    /// Providing access to `MetaData` is essential for our optimistic concurrency control.
222    fn metadata_mut(&mut self) -> &mut MetaData;
223
224    /// Returns all DB keys the object is referencing / depending on.
225    fn referenced_db_keys(&self) -> Vec<String>;
226
227    /// Checks if all fields (except references to other SclObjects) are legal initial fields.
228    /// Should be called before a create operation.
229    fn validate_fields_before_create(&self) -> Result<()>;
230
231    /// Checks if a proposed updated is valid. Should be called before a regular (**not**
232    /// related to finalizers / deletion) update operation.
233    fn validate_fields_before_update(
234        current_db_state: &Self,
235        proposed_new_state: &Self,
236    ) -> Result<()>;
237}
238
239#[derive(Clone, Debug, Deserialize, Eq, Ord, PartialEq, PartialOrd, Serialize)]
240#[cfg_attr(feature = "openapi", derive(OperationIo, JsonSchema))]
241#[cfg_attr(feature = "openapi", aide(output))]
242#[serde(try_from = "i64")]
243pub struct ResourceVersion(#[cfg_attr(feature = "openapi", validate(range(min = 1)))] i64);
244
245impl ResourceVersion {
246    pub fn value(&self) -> i64 {
247        self.0
248    }
249
250    pub fn inc(&mut self) -> Result<()> {
251        if self.0 == i64::MAX {
252            return Err(ValueSpaceExhaustedError::CannotIncreaseResourceVersion.into());
253        }
254
255        self.0 += 1;
256        Ok(())
257    }
258}
259
260impl Default for ResourceVersion {
261    fn default() -> Self {
262        Self(1)
263    }
264}
265
266impl TryFrom<i64> for ResourceVersion {
267    type Error = Error;
268
269    fn try_from(value: i64) -> Result<Self> {
270        if value > 0 {
271            Ok(Self(value))
272        } else {
273            Err(Error::ParsingFailed(
274                "Resource version must be greater than zero".to_string(),
275            ))
276        }
277    }
278}
279
280#[derive(Clone, Debug, Deserialize, Eq, Ord, PartialEq, PartialOrd, Serialize)]
281#[cfg_attr(feature = "openapi", derive(OperationIo, JsonSchema))]
282#[cfg_attr(feature = "openapi", aide(output))]
283#[serde(rename_all = "camelCase")]
284pub struct MetaData {
285    pub name: SclName,
286    #[serde(default)]
287    pub resource_version: ResourceVersion,
288
289    /// Will be set by the SCL API after it has received a delete request.
290    /// Controllers will clean up related resources before the actual delete
291    /// is performed.
292    #[serde(default)]
293    pub deletion_mark: Option<DeletionMark>,
294}
295
296impl MetaData {
297    pub fn new(name: SclName) -> Self {
298        Self {
299            name,
300            resource_version: ResourceVersion::default(),
301            deletion_mark: None,
302        }
303    }
304
305    /// Returns the value of the `name` field.
306    pub fn name(&self) -> &str {
307        self.name.as_str()
308    }
309
310    /// Indicates whether a safe deletion (without remaining dangling references elsewhere) should be possible.
311    pub fn may_be_deleted(&self) -> bool {
312        matches!(&self.deletion_mark, Some(dm) if dm.finalizers.may_be_deleted())
313    }
314
315    /// Indicates whether the object may be referenced by others. Currently, this is synonymous with
316    /// the absence of a deletion mark.
317    pub fn may_be_referenced(&self) -> bool {
318        // Consider to move this to SclObject if object specific circumstances should be considered, too.
319        self.deletion_mark.is_none()
320    }
321
322    /// Checks if all fields are legal initial fields. Should be called before a create operation.
323    pub fn validate_fields_before_create(&self) -> Result<()> {
324        if self.resource_version != ResourceVersion(1) {
325            return Err(Error::IllegalInitialValue(
326                "Illegal initial resource version!".to_string(),
327            ));
328        }
329
330        if self.deletion_mark.is_some() {
331            // We do not want the controllers to act on user supplied finalizers.
332            return Err(Error::IllegalInitialValue(
333                "The deletion mark must be initialized by the SCL API server".to_string(),
334            ));
335        }
336
337        Ok(())
338    }
339
340    /// Checks if a proposed updated is valid. Should be called before a regular (not related to
341    /// finalizers / deletion) update.
342    pub fn validate_fields_before_regular_update(
343        current_db_state: &Self,
344        proposed_new_state: &Self,
345    ) -> Result<()> {
346        if current_db_state.deletion_mark.is_some() || proposed_new_state.deletion_mark.is_some() {
347            return Err(Error::Application); // Function should not be called during finalizer updates.
348        }
349
350        if current_db_state.name != proposed_new_state.name {
351            return Err(TransitionError::Other(
352                "The name attribute may not be changed!".to_string(),
353            )
354            .into());
355        }
356
357        if current_db_state.resource_version.value() != proposed_new_state.resource_version.value()
358        {
359            return Err(TransitionError::Concurrency.into());
360        }
361
362        Ok(())
363    }
364
365    /// Checks if a proposed updated is valid. Should be called before a finalizer update.
366    /// Does not check [SclObject] specific details related to [FinalizerId]s.
367    pub fn validate_fields_before_finalizer_update(
368        current_db_state: &Self,
369        proposed_new_state: &Self,
370    ) -> Result<()> {
371        if current_db_state.name != proposed_new_state.name {
372            return Err(TransitionError::Other(
373                "The name attribute may not be changed!".to_string(),
374            )
375            .into());
376        }
377
378        if current_db_state.resource_version.value() != proposed_new_state.resource_version.value()
379        {
380            return Err(TransitionError::Concurrency.into());
381        }
382
383        match (
384            current_db_state.deletion_mark.as_ref(),
385            proposed_new_state.deletion_mark.as_ref(),
386        ) {
387            (Some(a), Some(b)) => {
388                Finalizers::identify_removed_finalizer(&a.finalizers, &b.finalizers).map(|_| ())
389            }
390            _ => Err(Error::Application), // Deletion must be in progress.
391        }
392    }
393}
394
395/// Indicates that certain work needs to be done by controllers before
396/// an [SclObject] can be safely (proper shutdown, no dangling references)
397/// deleted. Since SclObjects reference parent / owner SclObjects via
398/// [SclName]s, the work indicated by [FinalizerId] mainly affects children
399/// objects of a SclObject.
400#[derive(Clone, Debug, Deserialize, Eq, Ord, PartialEq, PartialOrd, Serialize)]
401#[cfg_attr(feature = "openapi", derive(OperationIo, JsonSchema))]
402#[cfg_attr(feature = "openapi", aide(output))]
403#[serde(rename_all = "camelCase")]
404pub enum FinalizerId {
405    /// All dependent VM API objects need to be removed before the referenced
406    /// API object (depending on the case the [SeparationContext], [Node] on
407    /// which the VM is running or VM [Volume]) may be deleted.
408    ForceRemoveVMs,
409
410    /// All dependent Volume API objects need to be removed before the parent
411    /// [SeparationContext] API object may be removed.
412    ForceRemoveVolumes,
413
414    /// All dependent Router API objects need to be removed before the parent
415    /// [SeparationContext] API object may be removed.
416    ForceRemoveRouters,
417
418    /// Network infrastructure related to the [SclObject] (e.g., network
419    /// namespaces and bridge devices for [SeparationContext]s, tap devices for
420    /// [VirtualMachine]s) needs to be removed up before the SclObject may be
421    /// removed.
422    CleanUpNetworkInfrastructure,
423
424    /// Local volume state needs to be removed before the [Volume] API object may
425    /// be deleted.
426    CleanUpVolumeState,
427
428    /// The VM needs to be removed.
429    RemoveVm,
430}
431
432/// A sequence of [FinalizerId]s that should be processed in the right order,
433/// one by one. Deletion may performed only if there is no FinalizerId left
434/// to process.
435///
436/// Similar to a Stack except that no new elements can be pushed to the data
437/// structure after it has been created.
438///
439/// # Examples
440///
441/// ```
442/// use scl_lib::api_objects::{Finalizers, FinalizerId::*};
443/// let fs = vec![ForceRemoveVMs, ForceRemoveVolumes];
444/// let mut finalizers = Finalizers::from(fs);
445/// assert_eq!(finalizers.take_next_finalizer(), Some(ForceRemoveVMs));
446/// assert_eq!(finalizers.next_finalizer(), Some(&ForceRemoveVolumes));
447/// let _ = finalizers.take_next_finalizer();
448/// assert!(finalizers.may_be_deleted());
449/// ```
450#[derive(Clone, Debug, Deserialize, Eq, Ord, PartialEq, PartialOrd, Serialize)]
451#[cfg_attr(feature = "openapi", derive(OperationIo, JsonSchema))]
452#[cfg_attr(feature = "openapi", aide(output))]
453#[serde(rename_all = "camelCase")]
454pub struct Finalizers {
455    inner: Vec<FinalizerId>,
456}
457
458impl Finalizers {
459    /// Returns true if no finalizer remains.
460    pub fn may_be_deleted(&self) -> bool {
461        self.inner.is_empty()
462    }
463
464    /// Returns a reference of the next [FinalizerId] that should be processed.
465    pub fn next_finalizer(&self) -> Option<&FinalizerId> {
466        self.inner.last()
467    }
468
469    /// Pops the next [FinalizerId] that should be processed.
470    pub fn take_next_finalizer(&mut self) -> Option<FinalizerId> {
471        self.inner.pop()
472    }
473
474    /// Identifies which finalizer has been removed.
475    ///
476    /// The function performs a transition validation since this is a prerequisite.
477    pub fn identify_removed_finalizer(
478        current_state: &Self,
479        proposed_new_state: &Self,
480    ) -> Result<FinalizerId> {
481        if proposed_new_state.inner.len() > current_state.inner.len() {
482            return Err(TransitionError::Other("Finalizers may not be added!".to_string()).into());
483        }
484
485        match current_state.inner.as_slice() {
486            // current_state is larger than proposed state, so `current_state.next_finalizer()`
487            // will return Some.
488            [elements @ .., last] if elements == proposed_new_state.inner => Ok(last.clone()),
489            _ => Err(TransitionError::Other(
490                "One finalizer must be removed at a time!".to_string(),
491            )
492            .into()),
493        }
494    }
495
496    /// Provides an iterator over the finalizers.
497    pub fn iter(&self) -> std::slice::Iter<'_, FinalizerId> {
498        self.inner.iter()
499    }
500}
501
502impl From<Vec<FinalizerId>> for Finalizers {
503    fn from(mut vec: Vec<FinalizerId>) -> Self {
504        vec.reverse();
505        Self { inner: vec }
506    }
507}
508
509/// Indicates an on-going deletion process.
510#[derive(Clone, Debug, Deserialize, Eq, Ord, PartialEq, PartialOrd, Serialize)]
511#[cfg_attr(feature = "openapi", derive(OperationIo, JsonSchema))]
512#[cfg_attr(feature = "openapi", aide(output))]
513#[serde(rename_all = "camelCase")]
514pub struct DeletionMark {
515    pub finalizers: Finalizers,
516    // requested_by: String,
517    // created_on: String
518}
519
520/// Common event classification
521#[derive(Clone, Debug, Deserialize, Eq, Ord, PartialEq, PartialOrd, Serialize)]
522#[cfg_attr(feature = "openapi", derive(OperationIo, JsonSchema))]
523#[cfg_attr(feature = "openapi", aide(output))]
524#[serde(rename_all = "camelCase")]
525#[serde(bound = "T: Serialize, for<'de2> T: Deserialize<'de2>")]
526pub enum SclEvent<T: SclObject> {
527    /// A new `SclObject` has been created. The payload will contain the new object data.
528    Created(T),
529    /// An `SclObject` has been updated. The payload will contain the new object state.
530    Updated(T),
531    /// An `SclObject` has been deleted. The payload will contain the last version of the object.
532    Deleted(T),
533    /// Non `SclObject` related general information. The payload contains an informative message.
534    Info(String),
535}
536
537/// Data type for wrapping additional informational messages
538#[derive(Clone, Debug, Deserialize, Eq, Ord, PartialEq, PartialOrd, Serialize)]
539#[cfg_attr(feature = "openapi", derive(OperationIo, JsonSchema))]
540#[cfg_attr(feature = "openapi", aide(output))]
541#[serde(rename_all = "camelCase")]
542pub struct SclInfo {
543    pub message: String,
544}
545
546impl SclInfo {
547    /// Constructs a [SclInfo] from a [&str] slice.
548    pub fn new(msg: &str) -> Self {
549        Self {
550            message: msg.into(),
551        }
552    }
553}
554
555#[derive(Clone, Debug, Deserialize, Eq, Ord, PartialEq, PartialOrd, Serialize)]
556#[cfg_attr(feature = "openapi", derive(OperationIo, JsonSchema))]
557#[cfg_attr(feature = "openapi", aide(output))]
558#[serde(try_from = "&str")]
559pub struct Url(#[cfg_attr(feature = "openapi", validate(url))] String);
560
561impl<'a> TryFrom<&'a str> for Url {
562    type Error = Error;
563
564    fn try_from(value: &'a str) -> std::result::Result<Self, Self::Error> {
565        reqwest::Url::try_from(value)
566            .map_err(|_e| TransitionError::Other("URL validation failed".to_string()))?;
567        Ok(Self(value.to_string()))
568    }
569}
570
571/// Returns a reference to the underlying string.
572impl AsRef<str> for Url {
573    fn as_ref(&self) -> &str {
574        &self.0
575    }
576}
577
578#[cfg(test)]
579mod test {
580    use super::*;
581    use serde_json;
582    use std::fs;
583    use std::path::Path;
584
585    /// Recursively traverses a directory and parses all JSON files into datatype `T`
586    pub fn parse_json_dir<T: serde::de::DeserializeOwned>(dir: &Path) -> Vec<T> {
587        let mut ts = vec![];
588        if dir.is_dir() {
589            for entry in fs::read_dir(dir).unwrap() {
590                let path = entry.unwrap().path();
591                if path.is_dir() {
592                    ts.append(&mut parse_json_dir::<T>(&path));
593                } else {
594                    let data = fs::read_to_string(path).unwrap();
595                    let t = serde_json::from_str::<T>(&data).unwrap();
596                    ts.push(t);
597                }
598            }
599        }
600
601        ts
602    }
603
604    #[test]
605    fn scl_names() {
606        // Result Ok
607        assert!(SclName::try_from("a").is_ok());
608        assert!(SclName::try_from("a-a").is_ok());
609        assert!(SclName::try_from("aa").is_ok());
610        assert!(SclName::try_from("a234").is_ok());
611        // Result Err
612        assert!(SclName::try_from("").is_err());
613        assert!(SclName::try_from("a-").is_err());
614        assert!(SclName::try_from("-a").is_err());
615        assert!(SclName::try_from("A-a").is_err());
616        assert!(SclName::try_from("a+ΓΌ4").is_err());
617        assert!(SclName::try_from("A234").is_err());
618        assert!(SclName::try_from("a".repeat(64)).is_err());
619    }
620
621    #[test]
622    fn resource_version() {
623        let mut rv = ResourceVersion(i64::MAX);
624        assert!(rv.inc().is_err());
625
626        assert!(ResourceVersion::try_from(-1).is_err());
627        assert!(ResourceVersion::try_from(0).is_err());
628        assert!(ResourceVersion::try_from(1).is_ok());
629        let v = ResourceVersion::default().value();
630        assert!(ResourceVersion::try_from(v).is_ok());
631    }
632
633    #[test]
634    fn metadata_may_be_deleted() {
635        let mut metadata = MetaData::new(SclName::try_from("example").unwrap());
636        assert!(!metadata.may_be_deleted());
637        metadata.deletion_mark = Some(DeletionMark {
638            finalizers: Finalizers::from(vec![FinalizerId::ForceRemoveVMs]),
639        });
640        assert!(!metadata.may_be_deleted());
641        metadata.deletion_mark = Some(DeletionMark {
642            finalizers: Finalizers::from(vec![]),
643        });
644        assert!(metadata.may_be_deleted());
645    }
646
647    #[test]
648    fn metadata_may_be_referenced() {
649        let mut metadata = MetaData::new(SclName::try_from("example").unwrap());
650        assert!(metadata.may_be_referenced());
651        metadata.deletion_mark = Some(DeletionMark {
652            finalizers: Finalizers::from(vec![FinalizerId::ForceRemoveVMs]),
653        });
654        assert!(!metadata.may_be_referenced());
655    }
656
657    #[test]
658    fn test_identify_removed_finalizer() {
659        use FinalizerId::*;
660        let state_0 =
661            Finalizers::from(vec![ForceRemoveVMs, ForceRemoveVolumes, ForceRemoveRouters]);
662        let state_1 = Finalizers::from(vec![ForceRemoveVolumes, ForceRemoveRouters]);
663        let state_2 = Finalizers::from(vec![ForceRemoveRouters]);
664        let state_3 = Finalizers::from(vec![]);
665        let state_1_wrong_items = Finalizers::from(vec![ForceRemoveRouters, ForceRemoveVolumes]);
666
667        assert_eq!(
668            Finalizers::identify_removed_finalizer(&state_0, &state_1),
669            Ok(ForceRemoveVMs)
670        );
671        assert_eq!(
672            Finalizers::identify_removed_finalizer(&state_1, &state_2),
673            Ok(ForceRemoveVolumes)
674        );
675        assert_eq!(
676            Finalizers::identify_removed_finalizer(&state_2, &state_3),
677            Ok(ForceRemoveRouters)
678        );
679        assert!(Finalizers::identify_removed_finalizer(&state_0, &state_0).is_err());
680        assert!(Finalizers::identify_removed_finalizer(&state_0, &state_3).is_err());
681        assert!(Finalizers::identify_removed_finalizer(&state_0, &state_1_wrong_items).is_err());
682        assert!(Finalizers::identify_removed_finalizer(&state_3, &state_2).is_err());
683        assert!(Finalizers::identify_removed_finalizer(&state_3, &state_3).is_err());
684    }
685
686    #[test]
687    fn scl_objects_can_be_parsed_as_metadata() {
688        // This is required for the deletion process / finalizer reduction to work.
689        //   The data models do not have a "kind" member (unlike k8s) that tells us,
690        //   how we should parse something. But everything can be parsed as metadata
691        //   if it has a metadata member that is "flattened" during de/serialization.
692        let _ms = parse_json_dir::<MetaData>(Path::new("../test/sample_json/"));
693    }
694
695    #[test]
696    fn url_parsing() {
697        assert!(Url::try_from("foo").is_err());
698        assert!(Url::try_from("bar.org").is_err());
699        assert!(Url::try_from("http://baz.org").is_ok());
700    }
701
702    #[test]
703    fn validate_metadata_fields_before_create() {
704        let mut m = MetaData::new(SclName::try_from("example").unwrap());
705        assert!(m.validate_fields_before_create().is_ok());
706        assert_eq!(m.resource_version, ResourceVersion(1));
707        m.resource_version.inc().unwrap();
708        assert!(m.validate_fields_before_create().is_err());
709
710        let mut m = MetaData::new(SclName::try_from("example").unwrap());
711        m.deletion_mark = Some(DeletionMark {
712            finalizers: Finalizers::from(vec![]),
713        });
714        assert!(m.validate_fields_before_create().is_err());
715    }
716
717    #[test]
718    fn validate_metadata_fields_before_regular_update() {
719        let m = MetaData::new(SclName::try_from("example").unwrap());
720
721        let mut n = m.clone();
722        n.resource_version.inc().unwrap();
723        assert!(MetaData::validate_fields_before_regular_update(&m, &n).is_err());
724
725        let mut n = m.clone();
726        n.name = SclName::try_from("changed").unwrap();
727        assert!(MetaData::validate_fields_before_regular_update(&m, &n).is_err());
728
729        let mut n = m.clone();
730        n.deletion_mark = Some(DeletionMark {
731            finalizers: Finalizers::from(vec![]),
732        });
733        assert!(MetaData::validate_fields_before_regular_update(&m, &n).is_err());
734    }
735
736    #[test]
737    fn validate_metadata_fields_before_finalizer_update() {
738        let m = MetaData {
739            name: SclName::try_from("example").unwrap(),
740            resource_version: Default::default(),
741            deletion_mark: Some(DeletionMark {
742                finalizers: Finalizers::from(vec![FinalizerId::ForceRemoveVMs]),
743            }),
744        };
745
746        let n = MetaData {
747            deletion_mark: Some(DeletionMark {
748                finalizers: Finalizers::from(vec![]),
749            }),
750            ..m.clone()
751        };
752
753        assert!(MetaData::validate_fields_before_finalizer_update(&m, &m).is_err());
754        assert!(MetaData::validate_fields_before_finalizer_update(&m, &n).is_ok());
755
756        let mut o = n.clone();
757        o.resource_version.inc().unwrap();
758        assert!(MetaData::validate_fields_before_finalizer_update(&m, &o).is_err());
759
760        let mut o = n.clone();
761        o.name = SclName::try_from("changed").unwrap();
762        assert!(MetaData::validate_fields_before_finalizer_update(&m, &o).is_err());
763
764        let mut o = n;
765        o.deletion_mark = None;
766        assert!(MetaData::validate_fields_before_finalizer_update(&m, &o).is_err());
767    }
768
769    /// Checks that metadata create validation is performed for a valid [SclObject].
770    pub fn detect_invalid_metadata_create_mutations<T: SclObject>(t: &T) {
771        #[allow(clippy::type_complexity)]
772        let invalid_mutations: Vec<Box<dyn Fn(&mut MetaData)>> = vec![
773            Box::new(|t| t.resource_version = ResourceVersion::try_from(2).unwrap()),
774            Box::new(|t| {
775                t.deletion_mark = Some(DeletionMark {
776                    finalizers: Finalizers::from(vec![]),
777                })
778            }),
779        ];
780
781        for transformation in invalid_mutations {
782            let mut t_clone = t.clone();
783            transformation(t_clone.metadata_mut());
784            assert!(t_clone.validate_fields_before_create().is_err());
785        }
786    }
787
788    /// Checks that metadata update validation is performed for a valid [SclObject].
789    pub fn detect_invalid_metadata_update_mutations<T: SclObject>(t: &T) {
790        #[allow(clippy::type_complexity)]
791        let invalid_mutations: Vec<Box<dyn Fn(&mut MetaData)>> = vec![
792            Box::new(|t| t.resource_version.inc().unwrap()),
793            Box::new(|t| t.name = SclName::try_from("changed").unwrap()),
794            Box::new(|t| {
795                t.deletion_mark = Some(DeletionMark {
796                    finalizers: Finalizers::from(vec![]),
797                })
798            }),
799        ];
800
801        for transformation in invalid_mutations {
802            let mut t_clone = t.clone();
803            let metadata = t_clone.metadata_mut();
804            transformation(metadata);
805            assert!(T::validate_fields_before_update(t, &t_clone).is_err())
806        }
807    }
808
809    /// Applies every mutation to an isolated clone of `t` and asserts that
810    /// `T::validate_fields_before_create` returns an error.
811    #[allow(clippy::type_complexity)]
812    pub fn detect_invalid_create_mutations<T: SclObject>(
813        t: &T,
814        mutations: Vec<Box<dyn Fn(&mut T)>>,
815    ) {
816        for mutation in mutations {
817            let mut t_clone = t.clone();
818            mutation(&mut t_clone);
819            assert!(T::validate_fields_before_create(&t_clone).is_err())
820        }
821    }
822
823    /// Applies every mutation to an isolated clone of `proposed` and asserts that
824    /// `T::validate_fields_before_update` with `current` and the mutated `proposed` returns an error.
825    #[allow(clippy::type_complexity)]
826    pub fn detect_invalid_update_mutations<T: SclObject>(
827        current: &T,
828        proposed: &T,
829        transformations: Vec<Box<dyn Fn(&mut T)>>,
830    ) {
831        for transformation in transformations {
832            let mut proposed_clone = proposed.clone();
833            transformation(&mut proposed_clone);
834            assert!(T::validate_fields_before_update(current, &proposed_clone).is_err())
835        }
836    }
837}