alien_core/
container_resources.rs1use crate::{
4 instance_catalog::{parse_cpu, parse_memory_bytes},
5 ComputeSettings, Container, ErrorData, ResourceChoiceRange, ResourceSpec, Stack,
6};
7use alien_error::{AlienError, Result};
8
9pub fn resolve_container_resources(
12 stack: &mut Stack,
13 settings: Option<&ComputeSettings>,
14) -> Result<(), ErrorData> {
15 if let Some(settings) = settings {
16 let mut ids: Vec<_> = settings.containers.keys().collect();
17 ids.sort();
18 for id in ids {
19 if !stack
20 .resources
21 .get(id)
22 .is_some_and(|entry| entry.config.downcast_ref::<Container>().is_some())
23 {
24 return Err(invalid(
25 id,
26 "resources",
27 "no such container in this release",
28 ));
29 }
30 }
31 }
32
33 let mut ids: Vec<_> = stack.resources.keys().cloned().collect();
34 ids.sort();
35 for id in ids {
36 let entry = stack.resources.get_mut(&id).expect("id came from map");
37 let Some(container) = entry.config.downcast_mut::<Container>() else {
38 continue;
39 };
40 let selected = settings.and_then(|settings| settings.containers.get(&id));
41 let cpu = selected
42 .and_then(|selected| selected.cpu.as_ref())
43 .map(ToString::to_string);
44 let memory = selected.and_then(|selected| selected.memory.as_deref());
45 let choices = container.resource_choices.as_ref();
46 resolve_dimension(
47 &id,
48 "cpu",
49 &mut container.cpu,
50 choices.and_then(|choices| choices.cpu.as_ref()),
51 cpu.as_deref(),
52 parse_positive_cpu,
53 )?;
54 resolve_dimension(
55 &id,
56 "memory",
57 &mut container.memory,
58 choices.and_then(|choices| choices.memory.as_ref()),
59 memory,
60 parse_positive_memory,
61 )?;
62 }
63 Ok(())
64}
65
66fn resolve_dimension<T: PartialOrd>(
67 id: &str,
68 dimension: &str,
69 spec: &mut ResourceSpec,
70 choices: Option<&ResourceChoiceRange>,
71 selected: Option<&str>,
72 parse: fn(&str) -> std::result::Result<T, String>,
73) -> Result<(), ErrorData> {
74 let parse_value = |value: &str| parse(value).map_err(|reason| invalid(id, dimension, &reason));
75 let Some(choices) = choices else {
76 if let Some(selected) = selected {
77 if parse_value(selected)? != parse_value(&spec.desired)? {
78 return Err(invalid(
79 id,
80 dimension,
81 "the release declares a fixed allocation",
82 ));
83 }
84 }
85 return Ok(());
86 };
87 let min = parse_value(&choices.min)?;
88 let max = parse_value(&choices.max)?;
89 let default = parse_value(&choices.default)?;
90 if min > max || default < min || default > max {
91 return Err(invalid(
92 id,
93 dimension,
94 "range must satisfy min <= default <= max",
95 ));
96 }
97 let value = selected.unwrap_or(&choices.default);
98 let allocation = parse_value(value)?;
99 if allocation < min || allocation > max {
100 return Err(invalid(
101 id,
102 dimension,
103 &format!(
104 "'{value}' is outside the allowed range {}..{}",
105 choices.min, choices.max,
106 ),
107 ));
108 }
109 spec.min = value.to_string();
111 spec.desired = value.to_string();
112 Ok(())
113}
114
115fn parse_positive_cpu(value: &str) -> std::result::Result<f64, String> {
116 let cpu = parse_cpu(value)?;
117 if !cpu.is_finite() || cpu <= 0.0 {
118 return Err(format!("CPU must be finite and positive, got '{value}'"));
119 }
120 Ok(cpu)
121}
122
123fn parse_positive_memory(value: &str) -> std::result::Result<u64, String> {
124 let numeric = ["Ki", "Mi", "Gi", "Ti"]
127 .iter()
128 .find_map(|suffix| value.strip_suffix(suffix))
129 .ok_or_else(|| format!("Memory must use Ki, Mi, Gi, or Ti, got '{value}'"))?;
130 let amount: f64 = numeric
131 .parse()
132 .map_err(|_| format!("Invalid memory '{value}'"))?;
133 if !amount.is_finite() || amount <= 0.0 {
134 return Err(format!("Memory must be finite and positive, got '{value}'"));
135 }
136 let bytes = parse_memory_bytes(value)?;
137 if bytes == 0 || bytes == u64::MAX {
138 return Err(format!("Memory is outside the supported range: '{value}'"));
139 }
140 Ok(bytes)
141}
142
143fn invalid(id: &str, dimension: &str, reason: &str) -> AlienError<ErrorData> {
144 AlienError::new(ErrorData::ContainerResourceSelectionInvalid {
145 resource_id: id.to_string(),
146 dimension: dimension.to_string(),
147 reason: reason.to_string(),
148 })
149}