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 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#[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
98pub trait SclObject:
101 Clone + DeserializeOwned + Eq + Ord + PartialEq + PartialOrd + Serialize
102{
103 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 fn name(&self) -> &SclName;
120
121 fn separation_context(&self) -> Option<&SclName> {
124 None
125 }
126
127 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 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 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 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 fn metadata(&self) -> &MetaData;
218
219 fn metadata_mut(&mut self) -> &mut MetaData;
221
222 fn referenced_db_keys(&self) -> Vec<String>;
224
225 fn validate_fields_before_create(&self) -> Result<()>;
228
229 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 #[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 pub fn name(&self) -> &str {
303 self.name.as_str()
304 }
305
306 pub fn may_be_deleted(&self) -> bool {
308 matches!(&self.deletion_mark, Some(dm) if dm.finalizers.may_be_deleted())
309 }
310
311 pub fn may_be_referenced(&self) -> bool {
314 self.deletion_mark.is_none()
316 }
317
318 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 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 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); }
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 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), }
388 }
389}
390
391#[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 ForceRemoveVMs,
404
405 ForceRemoveVolumes,
408
409 ForceRemoveRouters,
412
413 CleanUpNetworkInfrastructure,
418
419 CleanUpVolumeState,
422
423 RemoveVm,
425}
426
427#[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 pub fn may_be_deleted(&self) -> bool {
455 self.inner.is_empty()
456 }
457
458 pub fn next_finalizer(&self) -> Option<&FinalizerId> {
460 self.inner.last()
461 }
462
463 pub fn take_next_finalizer(&mut self) -> Option<FinalizerId> {
465 self.inner.pop()
466 }
467
468 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 [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 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#[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 }
512
513#[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 Created(T),
521 Updated(T),
523 Deleted(T),
525 Info(String),
527}
528
529#[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 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
561impl 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 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 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 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 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 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 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 #[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 #[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}