scl_lib/api_objects/
volume.rs

1// SPDX-License-Identifier: EUPL-1.2
2use crate::api_objects::{
3    Error, MetaData, Result, SclName, SclObject, SeparationContext, TransitionError, Url,
4};
5use serde::{Deserialize, Serialize};
6use std::path::{Path, PathBuf};
7
8#[cfg(feature = "openapi")]
9use aide::OperationIo;
10#[cfg(feature = "openapi")]
11use schemars::JsonSchema;
12
13#[derive(Clone, Debug, Deserialize, Eq, Ord, PartialEq, PartialOrd, Serialize)]
14#[cfg_attr(feature = "openapi", derive(OperationIo, JsonSchema))]
15#[cfg_attr(feature = "openapi", aide(output))]
16#[serde(rename_all = "camelCase")]
17pub struct Volume {
18    #[serde(flatten)]
19    pub metadata: MetaData,
20
21    /// Separation context which the volume is belonging to.
22    pub separation_context: SclName,
23
24    /// Size in mebibyte reserved for the volume.
25    #[cfg_attr(feature = "openapi", validate(range(min = 1)))]
26    #[serde(rename = "sizeMiB")]
27    pub size_mib: u64,
28
29    /// Latest status of the volume. Does not need to be specified by the user when
30    /// creating (POST), as it is initialized with the default values.
31    #[serde(default)]
32    pub status: VolumeStatus,
33
34    /// Optional URL pointing to initial data that should be copied into the new volume.
35    /// Make sure that the `size_mib` is at least as large as the required disk space.
36    pub url: Option<Url>,
37}
38
39impl Volume {
40    /// Returns the path where the volume file should be stored.
41    ///
42    /// Convenience wrapper around [derive_volume_file_path], see its documentation for more information.
43    ///
44    /// # Example
45    ///
46    /// ```
47    /// use crate::scl_lib::api_objects::{MetaData, SclName, Volume, derive_volume_file_path};
48    /// use std::path::Path;
49    /// use scl_lib::api_objects::SclObject;
50    ///
51    /// let vol = Volume {
52    ///     separation_context: SclName::try_from("sc01".to_string()).unwrap(),
53    ///     metadata: MetaData::new(SclName::try_from("vol01".to_string()).unwrap()),
54    ///     size_mib: 100,
55    ///     status: Default::default(),
56    ///     url: None,
57    /// };
58    /// let root = Path::new("/tmp/scl");
59    /// assert_eq!(vol.file_path(&root).as_path(), Path::new("/tmp/scl/sc01/volumes/vol01"));
60    /// assert_eq!(vol.file_path(&root), derive_volume_file_path(&root, &vol.separation_context, vol.name()))
61    /// ```
62    pub fn file_path(&self, root: &Path) -> PathBuf {
63        derive_volume_file_path(root, &self.separation_context, self.name())
64    }
65}
66
67/// Returns the path where a volume file should be stored.
68///
69/// Controllers interacting with the volume file must ensure a common understanding
70/// about `root`.
71///
72/// ```
73/// use crate::scl_lib::api_objects::{SclName, Volume, derive_volume_file_path};
74/// use std::path::Path;
75///
76/// let root = Path::new("/tmp/scl");
77/// let sc_name = SclName::try_from("sc01".to_string()).unwrap();
78/// let vol_name =  SclName::try_from("vol01".to_string()).unwrap();
79/// assert_eq!(derive_volume_file_path(&root, &sc_name, &vol_name), Path::new("/tmp/scl/sc01/volumes/vol01"));
80/// ```
81pub fn derive_volume_file_path(root: &Path, sc_name: &SclName, vol_name: &SclName) -> PathBuf {
82    let mut path = PathBuf::from(root);
83    path.push(format!("{}", sc_name));
84    path.push("volumes");
85    path.push(format!("{}", vol_name));
86    path
87}
88
89#[derive(Clone, Debug, Default, Deserialize, Eq, Ord, PartialEq, PartialOrd, Serialize)]
90#[cfg_attr(feature = "openapi", derive(OperationIo, JsonSchema))]
91#[cfg_attr(feature = "openapi", aide(output))]
92#[serde(rename_all = "camelCase")]
93pub enum VolumeStatus {
94    /// The volume needs to be initialized by the controller.
95    #[default]
96    Pending,
97
98    /// The volume was successfully initialized and is ready to use / already in use.
99    Active,
100
101    /// An error occurred during or after initialization. Possible reasons are for example:
102    ///
103    /// - insufficient disk space,
104    /// - the provided `url` could not be reached,
105    /// - the size of the initial data (via `url`) exceeded the `size_mib`.
106    Failed(String),
107}
108
109impl VolumeStatus {
110    fn transition_is_valid(old: &Self, new: &Self) -> bool {
111        use VolumeStatus::*;
112        matches!(
113            (old, new),
114            (&Pending, &Active) | (&Pending, &Failed(_)) | (&Active, &Failed(_))
115        )
116    }
117}
118
119impl SclObject for Volume {
120    /// Prefix for all [Volume]s (`"/volumes"`).
121    const PREFIX: &'static str = "/volumes";
122
123    fn name(&self) -> &SclName {
124        &self.metadata.name
125    }
126
127    fn separation_context(&self) -> Option<&SclName> {
128        Some(&self.separation_context)
129    }
130
131    fn metadata(&self) -> &MetaData {
132        &self.metadata
133    }
134
135    fn metadata_mut(&mut self) -> &mut MetaData {
136        &mut self.metadata
137    }
138
139    fn referenced_db_keys(&self) -> Vec<String> {
140        vec![SeparationContext::db_key(
141            self.separation_context.as_str(),
142            None,
143        )]
144    }
145
146    fn validate_fields_before_create(&self) -> Result<()> {
147        self.metadata.validate_fields_before_create()?;
148
149        if self.status != VolumeStatus::default() {
150            return Err(Error::IllegalInitialValue(
151                "Invalid initial volume status!".to_string(),
152            ));
153        }
154
155        if self.size_mib < 1 {
156            return Err(Error::IllegalInitialValue(
157                "Volume size must be greater than 0 MB!".to_string(),
158            ));
159        }
160
161        // No need to check the url field as the reachability or content size
162        // could change and need to be checked by the controller anyways.
163
164        Ok(())
165    }
166
167    fn validate_fields_before_update(
168        current_db_state: &Self,
169        proposed_new_state: &Self,
170    ) -> Result<()> {
171        MetaData::validate_fields_before_regular_update(
172            &current_db_state.metadata,
173            &proposed_new_state.metadata,
174        )?;
175
176        if current_db_state.separation_context != proposed_new_state.separation_context {
177            return Err(TransitionError::Other(
178                "Field separation_context may not be changed!".to_string(),
179            )
180            .into());
181        }
182
183        if current_db_state.size_mib != proposed_new_state.size_mib {
184            return Err(
185                TransitionError::Other("Field size may not be changed!".to_string()).into(),
186            );
187        }
188
189        if current_db_state.url != proposed_new_state.url {
190            return Err(TransitionError::Other("Field url may not be changed!".to_string()).into());
191        }
192
193        if !VolumeStatus::transition_is_valid(&current_db_state.status, &proposed_new_state.status)
194        {
195            return Err(TransitionError::Other(format!(
196                "Invalid status transition: Old({:?}) / New({:?})",
197                &current_db_state.status, &proposed_new_state.status
198            ))
199            .into());
200        }
201
202        Ok(())
203    }
204}
205
206#[cfg(test)]
207mod test {
208    use crate::api_objects::test::{
209        detect_invalid_metadata_create_mutations, detect_invalid_metadata_update_mutations,
210        detect_invalid_update_mutations, parse_json_dir,
211    };
212    use crate::api_objects::{MetaData, SclName, SclObject, Url, Volume, VolumeStatus};
213    use std::path::Path;
214
215    #[test]
216    fn parse_sample_json() {
217        parse_json_dir::<Volume>(Path::new("../test/sample_json/volumes"));
218    }
219
220    fn volume_status_transition_test_data() -> Vec<(VolumeStatus, VolumeStatus, bool)> {
221        use VolumeStatus::*;
222        vec![
223            (Pending, Pending, false),
224            (Pending, Active, true),
225            (Pending, Failed("foo".into()), true),
226            (Active, Pending, false),
227            (Active, Active, false),
228            (Active, Failed("foo".into()), true),
229            (Failed("foo".into()), Pending, false),
230            (Failed("foo".into()), Active, false),
231            (Failed("foo".into()), Failed("foo".into()), false),
232        ]
233    }
234
235    #[test]
236    fn test_volume_status_transition() {
237        for (old, new, expected_result) in volume_status_transition_test_data() {
238            assert_eq!(
239                VolumeStatus::transition_is_valid(&old, &new),
240                expected_result
241            );
242        }
243    }
244
245    fn example_volume() -> Volume {
246        Volume {
247            metadata: MetaData::new(SclName::try_from("example".to_string()).unwrap()),
248            separation_context: SclName::try_from("example".to_string()).unwrap(),
249            size_mib: 1,
250            status: Default::default(),
251            url: None,
252        }
253    }
254
255    #[test]
256    fn validate_fields_before_create() {
257        let mut vol = example_volume();
258        assert!(vol.validate_fields_before_create().is_ok());
259
260        vol.status = VolumeStatus::Active;
261        assert!(vol.validate_fields_before_create().is_err()); // Invalid initial state.
262
263        let mut vol = example_volume();
264        vol.size_mib = 0;
265        assert!(vol.validate_fields_before_create().is_err()); // Size must be greater than 0.
266    }
267
268    #[test]
269    fn validate_fields_before_update() {
270        let mut current = example_volume();
271        let mut proposed = example_volume();
272        proposed.status = VolumeStatus::Active;
273        assert!(Volume::validate_fields_before_update(&current, &proposed).is_ok());
274
275        #[allow(clippy::type_complexity)]
276        let invalid_mutations: Vec<Box<dyn Fn(&mut Volume)>> = vec![
277            Box::new(|t| t.separation_context = SclName::try_from("different").unwrap()),
278            Box::new(|t| t.size_mib = 9999),
279            Box::new(|t| t.url = Some(Url::try_from("http://d3tn.com").unwrap())),
280        ];
281
282        detect_invalid_update_mutations(&current, &proposed, invalid_mutations);
283
284        // Check that status transition validation is checked.
285        for (old, new, expected_result) in volume_status_transition_test_data() {
286            current.status = old;
287            proposed.status = new;
288            assert_eq!(
289                Volume::validate_fields_before_update(&current, &proposed).is_ok(),
290                expected_result
291            );
292        }
293    }
294
295    #[test]
296    fn detect_invalid_metadata_mutations() {
297        let t = example_volume();
298        detect_invalid_metadata_create_mutations(&t);
299        detect_invalid_metadata_update_mutations(&t);
300    }
301}