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