scl_lib/api_objects/
node.rs

1// SPDX-License-Identifier: EUPL-1.2
2use crate::api_objects::{Error, MetaData, Result, SclName, SclObject, TransitionError, Url};
3use serde::{Deserialize, Serialize};
4
5#[cfg(feature = "openapi")]
6use aide::OperationIo;
7#[cfg(feature = "openapi")]
8use schemars::JsonSchema;
9
10#[derive(Clone, Debug, Deserialize, Eq, Ord, PartialEq, PartialOrd, Serialize)]
11#[cfg_attr(feature = "openapi", derive(OperationIo, JsonSchema))]
12#[cfg_attr(feature = "openapi", aide(output))]
13#[serde(rename_all = "camelCase")]
14pub struct Node {
15    #[serde(flatten)]
16    pub metadata: MetaData,
17    /// Total amount of computational resources offered by the Node
18    pub resources: Resources,
19    /// Endpoint of the Node API
20    pub node_api: Endpoint,
21    /// Endpoint of the SmartNIC API
22    pub nic_api: Endpoint,
23    /// Latest status of the node. Does not need to be specified by the user when
24    /// creating (POST), as it is initialized with the default values.
25    #[serde(default)]
26    pub status: NodeStatus,
27}
28
29impl SclObject for Node {
30    /// Prefix for all [Node]s (`"/nodes"`).
31    const PREFIX: &'static str = "/nodes";
32
33    fn name(&self) -> &SclName {
34        &self.metadata.name
35    }
36
37    /// Overrides the default implementation of the [SclObject] trait to ignore the `sc` argument
38    /// when constructing API endpoints.
39    ///
40    /// # Examples
41    ///
42    /// ```
43    /// use scl_lib::api_objects::{Node, SclObject};
44    ///
45    /// assert_eq!(Node::api_endpoint(None, "node-01"), "/nodes/node-01");
46    /// assert_eq!(Node::api_endpoint(None, "node-01"), Node::api_endpoint("node-01", "node-01"));
47    /// assert_eq!(Node::api_endpoint("node-01", None), Node::api_endpoint(None, None));
48    /// assert_eq!(Node::api_endpoint(None, None), Node::PREFIX);
49    /// ```
50    fn api_endpoint<'a>(
51        _sc: impl Into<Option<&'a str>>,
52        name: impl Into<Option<&'a str>>,
53    ) -> String {
54        if let Some(name) = name.into() {
55            format!("{}/{}", Self::PREFIX, name)
56        } else {
57            Self::PREFIX.into()
58        }
59    }
60
61    /// Overrides the default implementation of the [SclObject] trait to use either the `sc` or the
62    /// `name` argument for DB key construction.
63    ///
64    /// # Examples
65    ///
66    /// ```
67    /// use scl_lib::api_objects::{Node, SclObject};
68    ///
69    /// assert_eq!(Node::db_key(None, "node-01"), "/nodes/node-01");
70    /// assert_eq!(Node::db_key(None, "node-01"), Node::db_key("node-01", "node-01"));
71    /// assert_eq!(Node::db_key("node-01", None), Node::db_key(None, None));
72    /// assert_eq!(Node::db_key(None, None), Node::PREFIX);
73    /// ```
74    fn db_key<'a>(_sc: impl Into<Option<&'a str>>, name: impl Into<Option<&'a str>>) -> String {
75        if let Some(name) = name.into() {
76            format!("{}/{}", Self::PREFIX, name)
77        } else {
78            Self::PREFIX.into()
79        }
80    }
81
82    fn metadata(&self) -> &MetaData {
83        &self.metadata
84    }
85
86    fn metadata_mut(&mut self) -> &mut MetaData {
87        &mut self.metadata
88    }
89
90    fn referenced_db_keys(&self) -> Vec<String> {
91        vec![]
92    }
93
94    fn validate_fields_before_create(&self) -> Result<()> {
95        self.metadata.validate_fields_before_create()?;
96
97        if !self.resources.all_resources_are_greater_than_zero() {
98            return Err(Error::IllegalInitialValue(
99                "All resources must be greater than 0!".to_string(),
100            ));
101        }
102
103        Ok(())
104    }
105
106    fn validate_fields_before_update(
107        current_db_state: &Self,
108        proposed_new_state: &Self,
109    ) -> Result<()> {
110        MetaData::validate_fields_before_regular_update(
111            &current_db_state.metadata,
112            &proposed_new_state.metadata,
113        )?;
114
115        if current_db_state.nic_api != proposed_new_state.nic_api
116            || current_db_state.node_api != proposed_new_state.node_api
117            || current_db_state.resources != proposed_new_state.resources
118        {
119            Err(TransitionError::Other("Only status field may be updated!".to_string()).into())
120        } else {
121            Ok(())
122        }
123    }
124}
125
126#[derive(Clone, Default, Debug, Deserialize, Eq, Ord, PartialEq, PartialOrd, Serialize)]
127#[cfg_attr(feature = "openapi", derive(OperationIo, JsonSchema))]
128#[cfg_attr(feature = "openapi", aide(output))]
129#[serde(rename_all = "camelCase")]
130pub struct Resources {
131    /// Number of vCPUs.
132    #[cfg_attr(feature = "openapi", validate(range(min = 1)))]
133    pub vcpu: u16,
134    /// Amount of RAM in mebibyte.
135    #[cfg_attr(feature = "openapi", validate(range(min = 1)))]
136    #[serde(rename = "ramMiB")]
137    pub ram_mib: u32,
138}
139
140impl Resources {
141    pub fn all_resources_are_greater_than_zero(&self) -> bool {
142        self.vcpu > 0 && self.ram_mib > 0
143    }
144}
145
146#[derive(Clone, Debug, Deserialize, Eq, Ord, PartialEq, PartialOrd, Serialize)]
147#[cfg_attr(feature = "openapi", derive(OperationIo, JsonSchema))]
148#[cfg_attr(feature = "openapi", aide(output))]
149#[serde(rename_all = "camelCase")]
150pub struct Endpoint {
151    /// Address of the endpoint.
152    pub url: Url,
153
154    /// API version supported by the endpoint
155    pub version: u8, // TODO use enum instead. Or Vec of enums?
156}
157
158#[derive(Clone, Debug, Default, Deserialize, Eq, Ord, PartialEq, PartialOrd, Serialize)]
159#[cfg_attr(feature = "openapi", derive(OperationIo, JsonSchema))]
160#[cfg_attr(feature = "openapi", aide(output))]
161#[serde(rename_all = "camelCase")]
162pub struct NodeStatus {
163    pub last_heartbeat: String, // TODO u64?
164    pub healthy: bool,
165}
166
167#[cfg(test)]
168mod test {
169    use super::Node;
170    use crate::api_objects::test::{
171        detect_invalid_metadata_create_mutations, detect_invalid_metadata_update_mutations,
172        detect_invalid_update_mutations, parse_json_dir,
173    };
174    use crate::api_objects::{Endpoint, MetaData, Resources, SclName, SclObject, Url};
175    use std::path::Path;
176
177    #[test]
178    fn parse_sample_json() {
179        parse_json_dir::<Node>(Path::new("../test/sample_json/nodes"));
180    }
181
182    fn example_node() -> Node {
183        let endpoint = Endpoint {
184            url: Url::try_from("ftp://example.com").unwrap(),
185            version: 1,
186        };
187
188        Node {
189            metadata: MetaData::new(SclName::try_from("example").unwrap()),
190            resources: Resources {
191                vcpu: 1,
192                ram_mib: 1,
193            },
194            node_api: endpoint.clone(),
195            nic_api: endpoint,
196            status: Default::default(),
197        }
198    }
199
200    #[test]
201    fn validate_fields_before_create() {
202        let mut node = example_node();
203        assert!(node.validate_fields_before_create().is_ok());
204        node.resources.vcpu = 0;
205        assert!(node.validate_fields_before_create().is_err());
206        node.resources.vcpu = 1;
207        node.resources.ram_mib = 0;
208        assert!(node.validate_fields_before_create().is_err());
209    }
210
211    #[test]
212    fn validate_fields_before_update() {
213        let current = example_node();
214        assert!(Node::validate_fields_before_update(&current, &current).is_ok());
215
216        #[allow(clippy::type_complexity)]
217        let invalid_mutations: Vec<Box<dyn Fn(&mut Node)>> = vec![
218            Box::new(|t| t.resources.ram_mib = 999),
219            Box::new(|t| t.resources.vcpu = 999),
220            Box::new(|t| t.nic_api.url = Url::try_from("ftp://other.com").unwrap()),
221            Box::new(|t| t.nic_api.version = 123),
222            Box::new(|t| t.node_api.url = Url::try_from("ftp://other.com").unwrap()),
223            Box::new(|t| t.node_api.version = 123),
224        ];
225
226        detect_invalid_update_mutations(&current, &current, invalid_mutations);
227    }
228
229    #[test]
230    fn detect_invalid_metadata_mutations() {
231        let t = example_node();
232        detect_invalid_metadata_create_mutations(&t);
233        detect_invalid_metadata_update_mutations(&t);
234    }
235
236    #[test]
237    fn test_all_resources_are_greater_than_zero() {
238        let mut r = Resources {
239            vcpu: 0,
240            ram_mib: 0,
241        };
242        assert!(!r.all_resources_are_greater_than_zero());
243        r.vcpu = 1;
244        assert!(!r.all_resources_are_greater_than_zero());
245        r.ram_mib = 1;
246        assert!(r.all_resources_are_greater_than_zero());
247        r.vcpu = 0;
248        assert!(!r.all_resources_are_greater_than_zero());
249    }
250}