zombienet_configuration/shared/
resources.rs1use std::error::Error;
2
3use lazy_static::lazy_static;
4use regex::Regex;
5use serde::{
6 de::{self},
7 ser::SerializeStruct,
8 Deserialize, Serialize,
9};
10use support::constants::{SHOULD_COMPILE, THIS_IS_A_BUG};
11
12use super::{
13 errors::{ConversionError, FieldError},
14 helpers::merge_errors,
15};
16
17#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
37pub struct ResourceQuantity(String);
38
39impl ResourceQuantity {
40 pub fn as_str(&self) -> &str {
41 &self.0
42 }
43}
44
45impl TryFrom<&str> for ResourceQuantity {
46 type Error = ConversionError;
47
48 fn try_from(value: &str) -> Result<Self, Self::Error> {
49 lazy_static! {
50 static ref RE: Regex = Regex::new(r"^\d+(.\d+)?(m|K|M|G|T|P|E|Ki|Mi|Gi|Ti|Pi|Ei)?$")
51 .expect(&format!("{SHOULD_COMPILE}, {THIS_IS_A_BUG}"));
52 }
53
54 if !RE.is_match(value) {
55 return Err(ConversionError::DoesntMatchRegex {
56 value: value.to_string(),
57 regex: r"^\d+(.\d+)?(m|K|M|G|T|P|E|Ki|Mi|Gi|Ti|Pi|Ei)?$".to_string(),
58 });
59 }
60
61 Ok(Self(value.to_string()))
62 }
63}
64
65impl From<u64> for ResourceQuantity {
66 fn from(value: u64) -> Self {
67 Self(value.to_string())
68 }
69}
70
71#[derive(Debug, Default, Clone, PartialEq)]
73pub struct Resources {
74 request_memory: Option<ResourceQuantity>,
75 request_cpu: Option<ResourceQuantity>,
76 limit_memory: Option<ResourceQuantity>,
77 limit_cpu: Option<ResourceQuantity>,
78}
79
80#[derive(Serialize, Deserialize)]
81struct ResourcesField {
82 memory: Option<ResourceQuantity>,
83 cpu: Option<ResourceQuantity>,
84}
85
86impl Serialize for Resources {
87 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
88 where
89 S: serde::Serializer,
90 {
91 let mut state = serializer.serialize_struct("Resources", 2)?;
92
93 if self.request_memory.is_some() || self.request_memory.is_some() {
94 state.serialize_field(
95 "requests",
96 &ResourcesField {
97 memory: self.request_memory.clone(),
98 cpu: self.request_cpu.clone(),
99 },
100 )?;
101 } else {
102 state.skip_field("requests")?;
103 }
104
105 if self.limit_memory.is_some() || self.limit_memory.is_some() {
106 state.serialize_field(
107 "limits",
108 &ResourcesField {
109 memory: self.limit_memory.clone(),
110 cpu: self.limit_cpu.clone(),
111 },
112 )?;
113 } else {
114 state.skip_field("limits")?;
115 }
116
117 state.end()
118 }
119}
120
121struct ResourcesVisitor;
122
123impl<'de> de::Visitor<'de> for ResourcesVisitor {
124 type Value = Resources;
125
126 fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
127 formatter.write_str("a resources object")
128 }
129
130 fn visit_map<A>(self, mut map: A) -> Result<Self::Value, A::Error>
131 where
132 A: de::MapAccess<'de>,
133 {
134 let mut resources: Resources = Resources::default();
135
136 while let Some((key, value)) = map.next_entry::<String, ResourcesField>()? {
137 match key.as_str() {
138 "requests" => {
139 resources.request_memory = value.memory;
140 resources.request_cpu = value.cpu;
141 },
142 "limits" => {
143 resources.limit_memory = value.memory;
144 resources.limit_cpu = value.cpu;
145 },
146 _ => {
147 return Err(de::Error::unknown_field(
148 &key,
149 &["requests", "limits", "cpu", "memory"],
150 ))
151 },
152 }
153 }
154 Ok(resources)
155 }
156}
157
158impl<'de> Deserialize<'de> for Resources {
159 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
160 where
161 D: serde::Deserializer<'de>,
162 {
163 deserializer.deserialize_any(ResourcesVisitor)
164 }
165}
166
167impl Resources {
168 pub fn request_memory(&self) -> Option<&ResourceQuantity> {
170 self.request_memory.as_ref()
171 }
172
173 pub fn request_cpu(&self) -> Option<&ResourceQuantity> {
175 self.request_cpu.as_ref()
176 }
177
178 pub fn limit_memory(&self) -> Option<&ResourceQuantity> {
180 self.limit_memory.as_ref()
181 }
182
183 pub fn limit_cpu(&self) -> Option<&ResourceQuantity> {
185 self.limit_cpu.as_ref()
186 }
187}
188
189#[derive(Debug, Default)]
191pub struct ResourcesBuilder {
192 config: Resources,
193 errors: Vec<anyhow::Error>,
194}
195
196impl ResourcesBuilder {
197 pub fn new() -> ResourcesBuilder {
198 Self::default()
199 }
200
201 fn transition(config: Resources, errors: Vec<anyhow::Error>) -> Self {
202 Self { config, errors }
203 }
204
205 pub fn with_request_memory<T>(self, quantity: T) -> Self
207 where
208 T: TryInto<ResourceQuantity>,
209 T::Error: Error + Send + Sync + 'static,
210 {
211 match quantity.try_into() {
212 Ok(quantity) => Self::transition(
213 Resources {
214 request_memory: Some(quantity),
215 ..self.config
216 },
217 self.errors,
218 ),
219 Err(error) => Self::transition(
220 self.config,
221 merge_errors(self.errors, FieldError::RequestMemory(error.into()).into()),
222 ),
223 }
224 }
225
226 pub fn with_request_cpu<T>(self, quantity: T) -> Self
228 where
229 T: TryInto<ResourceQuantity>,
230 T::Error: Error + Send + Sync + 'static,
231 {
232 match quantity.try_into() {
233 Ok(quantity) => Self::transition(
234 Resources {
235 request_cpu: Some(quantity),
236 ..self.config
237 },
238 self.errors,
239 ),
240 Err(error) => Self::transition(
241 self.config,
242 merge_errors(self.errors, FieldError::RequestCpu(error.into()).into()),
243 ),
244 }
245 }
246
247 pub fn with_limit_memory<T>(self, quantity: T) -> Self
249 where
250 T: TryInto<ResourceQuantity>,
251 T::Error: Error + Send + Sync + 'static,
252 {
253 match quantity.try_into() {
254 Ok(quantity) => Self::transition(
255 Resources {
256 limit_memory: Some(quantity),
257 ..self.config
258 },
259 self.errors,
260 ),
261 Err(error) => Self::transition(
262 self.config,
263 merge_errors(self.errors, FieldError::LimitMemory(error.into()).into()),
264 ),
265 }
266 }
267
268 pub fn with_limit_cpu<T>(self, quantity: T) -> Self
270 where
271 T: TryInto<ResourceQuantity>,
272 T::Error: Error + Send + Sync + 'static,
273 {
274 match quantity.try_into() {
275 Ok(quantity) => Self::transition(
276 Resources {
277 limit_cpu: Some(quantity),
278 ..self.config
279 },
280 self.errors,
281 ),
282 Err(error) => Self::transition(
283 self.config,
284 merge_errors(self.errors, FieldError::LimitCpu(error.into()).into()),
285 ),
286 }
287 }
288
289 pub fn build(self) -> Result<Resources, Vec<anyhow::Error>> {
291 if !self.errors.is_empty() {
292 return Err(self.errors);
293 }
294
295 Ok(self.config)
296 }
297}
298
299#[cfg(test)]
300#[allow(non_snake_case)]
301mod tests {
302 use super::*;
303 use crate::NetworkConfig;
304
305 macro_rules! impl_resources_quantity_unit_test {
306 ($val:literal) => {{
307 let resources = ResourcesBuilder::new()
308 .with_request_memory($val)
309 .build()
310 .unwrap();
311
312 assert_eq!(resources.request_memory().unwrap().as_str(), $val);
313 assert_eq!(resources.request_cpu(), None);
314 assert_eq!(resources.limit_cpu(), None);
315 assert_eq!(resources.limit_memory(), None);
316 }};
317 }
318
319 #[test]
320 fn converting_a_string_a_resource_quantity_without_unit_should_succeeds() {
321 impl_resources_quantity_unit_test!("1000");
322 }
323
324 #[test]
325 fn converting_a_str_with_m_unit_into_a_resource_quantity_should_succeeds() {
326 impl_resources_quantity_unit_test!("100m");
327 }
328
329 #[test]
330 fn converting_a_str_with_K_unit_into_a_resource_quantity_should_succeeds() {
331 impl_resources_quantity_unit_test!("50K");
332 }
333
334 #[test]
335 fn converting_a_str_with_M_unit_into_a_resource_quantity_should_succeeds() {
336 impl_resources_quantity_unit_test!("100M");
337 }
338
339 #[test]
340 fn converting_a_str_with_G_unit_into_a_resource_quantity_should_succeeds() {
341 impl_resources_quantity_unit_test!("1G");
342 }
343
344 #[test]
345 fn converting_a_str_with_T_unit_into_a_resource_quantity_should_succeeds() {
346 impl_resources_quantity_unit_test!("0.01T");
347 }
348
349 #[test]
350 fn converting_a_str_with_P_unit_into_a_resource_quantity_should_succeeds() {
351 impl_resources_quantity_unit_test!("0.00001P");
352 }
353
354 #[test]
355 fn converting_a_str_with_E_unit_into_a_resource_quantity_should_succeeds() {
356 impl_resources_quantity_unit_test!("0.000000001E");
357 }
358
359 #[test]
360 fn converting_a_str_with_Ki_unit_into_a_resource_quantity_should_succeeds() {
361 impl_resources_quantity_unit_test!("50Ki");
362 }
363
364 #[test]
365 fn converting_a_str_with_Mi_unit_into_a_resource_quantity_should_succeeds() {
366 impl_resources_quantity_unit_test!("100Mi");
367 }
368
369 #[test]
370 fn converting_a_str_with_Gi_unit_into_a_resource_quantity_should_succeeds() {
371 impl_resources_quantity_unit_test!("1Gi");
372 }
373
374 #[test]
375 fn converting_a_str_with_Ti_unit_into_a_resource_quantity_should_succeeds() {
376 impl_resources_quantity_unit_test!("0.01Ti");
377 }
378
379 #[test]
380 fn converting_a_str_with_Pi_unit_into_a_resource_quantity_should_succeeds() {
381 impl_resources_quantity_unit_test!("0.00001Pi");
382 }
383
384 #[test]
385 fn converting_a_str_with_Ei_unit_into_a_resource_quantity_should_succeeds() {
386 impl_resources_quantity_unit_test!("0.000000001Ei");
387 }
388
389 #[test]
390 fn resources_config_builder_should_succeeds_and_returns_a_resources_config() {
391 let resources = ResourcesBuilder::new()
392 .with_request_memory("200M")
393 .with_request_cpu("1G")
394 .with_limit_cpu("500M")
395 .with_limit_memory("2G")
396 .build()
397 .unwrap();
398
399 assert_eq!(resources.request_memory().unwrap().as_str(), "200M");
400 assert_eq!(resources.request_cpu().unwrap().as_str(), "1G");
401 assert_eq!(resources.limit_cpu().unwrap().as_str(), "500M");
402 assert_eq!(resources.limit_memory().unwrap().as_str(), "2G");
403 }
404
405 #[test]
406 fn resources_config_toml_import_should_succeeds_and_returns_a_resources_config() {
407 let load_from_toml =
408 NetworkConfig::load_from_toml("./testing/snapshots/0001-big-network.toml").unwrap();
409
410 let resources = load_from_toml.relaychain().default_resources().unwrap();
411 assert_eq!(resources.request_memory().unwrap().as_str(), "500M");
412 assert_eq!(resources.request_cpu().unwrap().as_str(), "100000");
413 assert_eq!(resources.limit_cpu().unwrap().as_str(), "10Gi");
414 assert_eq!(resources.limit_memory().unwrap().as_str(), "4000M");
415 }
416
417 #[test]
418 fn resources_config_builder_should_fails_and_returns_an_error_if_couldnt_parse_request_memory()
419 {
420 let resources_builder = ResourcesBuilder::new().with_request_memory("invalid");
421
422 let errors = resources_builder.build().err().unwrap();
423
424 assert_eq!(errors.len(), 1);
425 assert_eq!(
426 errors.first().unwrap().to_string(),
427 r"request_memory: 'invalid' doesn't match regex '^\d+(.\d+)?(m|K|M|G|T|P|E|Ki|Mi|Gi|Ti|Pi|Ei)?$'"
428 );
429 }
430
431 #[test]
432 fn resources_config_builder_should_fails_and_returns_an_error_if_couldnt_parse_request_cpu() {
433 let resources_builder = ResourcesBuilder::new().with_request_cpu("invalid");
434
435 let errors = resources_builder.build().err().unwrap();
436
437 assert_eq!(errors.len(), 1);
438 assert_eq!(
439 errors.first().unwrap().to_string(),
440 r"request_cpu: 'invalid' doesn't match regex '^\d+(.\d+)?(m|K|M|G|T|P|E|Ki|Mi|Gi|Ti|Pi|Ei)?$'"
441 );
442 }
443
444 #[test]
445 fn resources_config_builder_should_fails_and_returns_an_error_if_couldnt_parse_limit_memory() {
446 let resources_builder = ResourcesBuilder::new().with_limit_memory("invalid");
447
448 let errors = resources_builder.build().err().unwrap();
449
450 assert_eq!(errors.len(), 1);
451 assert_eq!(
452 errors.first().unwrap().to_string(),
453 r"limit_memory: 'invalid' doesn't match regex '^\d+(.\d+)?(m|K|M|G|T|P|E|Ki|Mi|Gi|Ti|Pi|Ei)?$'"
454 );
455 }
456
457 #[test]
458 fn resources_config_builder_should_fails_and_returns_an_error_if_couldnt_parse_limit_cpu() {
459 let resources_builder = ResourcesBuilder::new().with_limit_cpu("invalid");
460
461 let errors = resources_builder.build().err().unwrap();
462
463 assert_eq!(errors.len(), 1);
464 assert_eq!(
465 errors.first().unwrap().to_string(),
466 r"limit_cpu: 'invalid' doesn't match regex '^\d+(.\d+)?(m|K|M|G|T|P|E|Ki|Mi|Gi|Ti|Pi|Ei)?$'"
467 );
468 }
469
470 #[test]
471 fn resources_config_builder_should_fails_and_returns_multiple_error_if_couldnt_parse_multiple_fields(
472 ) {
473 let resources_builder = ResourcesBuilder::new()
474 .with_limit_cpu("invalid")
475 .with_request_memory("invalid");
476
477 let errors = resources_builder.build().err().unwrap();
478
479 assert_eq!(errors.len(), 2);
480 assert_eq!(
481 errors.first().unwrap().to_string(),
482 r"limit_cpu: 'invalid' doesn't match regex '^\d+(.\d+)?(m|K|M|G|T|P|E|Ki|Mi|Gi|Ti|Pi|Ei)?$'"
483 );
484 assert_eq!(
485 errors.get(1).unwrap().to_string(),
486 r"request_memory: 'invalid' doesn't match regex '^\d+(.\d+)?(m|K|M|G|T|P|E|Ki|Mi|Gi|Ti|Pi|Ei)?$'"
487 );
488 }
489}