1use 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#[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(); 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
100pub trait SclObject:
103 Clone + DeserializeOwned + Eq + Ord + PartialEq + PartialOrd + Serialize
104{
105 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 fn name(&self) -> &SclName;
122
123 fn separation_context(&self) -> Option<&SclName> {
126 None
127 }
128
129 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 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 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 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 fn metadata(&self) -> &MetaData;
220
221 fn metadata_mut(&mut self) -> &mut MetaData;
223
224 fn referenced_db_keys(&self) -> Vec<String>;
226
227 fn validate_fields_before_create(&self) -> Result<()>;
230
231 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 #[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 pub fn name(&self) -> &str {
307 self.name.as_str()
308 }
309
310 pub fn may_be_deleted(&self) -> bool {
312 matches!(&self.deletion_mark, Some(dm) if dm.finalizers.may_be_deleted())
313 }
314
315 pub fn may_be_referenced(&self) -> bool {
318 self.deletion_mark.is_none()
320 }
321
322 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 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 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); }
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 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), }
392 }
393}
394
395#[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 ForceRemoveVMs,
409
410 ForceRemoveVolumes,
413
414 ForceRemoveRouters,
417
418 CleanUpNetworkInfrastructure,
423
424 CleanUpVolumeState,
427
428 RemoveVm,
430}
431
432#[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 pub fn may_be_deleted(&self) -> bool {
461 self.inner.is_empty()
462 }
463
464 pub fn next_finalizer(&self) -> Option<&FinalizerId> {
466 self.inner.last()
467 }
468
469 pub fn take_next_finalizer(&mut self) -> Option<FinalizerId> {
471 self.inner.pop()
472 }
473
474 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 [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 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#[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 }
519
520#[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 Created(T),
529 Updated(T),
531 Deleted(T),
533 Info(String),
535}
536
537#[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 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
571impl 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 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 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 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 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 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 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 #[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 #[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}