1use crate::{
4 instance_catalog::{parse_cpu, parse_memory_bytes},
5 ComputeSettings, Container, ErrorData, ResourceChoiceRange, ResourceSpec, Stack, StackState,
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 if let Some(size) = selected.and_then(|selected| selected.volume_size_gib) {
46 let storage = container.persistent_storage.as_mut().ok_or_else(|| {
47 invalid(
48 &id,
49 "volumeSizeGib",
50 "the release declares no persistent volume",
51 )
52 })?;
53 if size == 0 || size > 65536 {
54 return Err(invalid(
55 &id,
56 "volumeSizeGib",
57 "capacity must be between 1 and 65536 GiB",
58 ));
59 }
60 let selected = format!("{size}Gi");
61 if parse_memory_bytes(&storage.size) != parse_memory_bytes(&selected) {
63 storage.size = selected;
64 }
65 }
66 let choices = container.resource_choices.as_ref();
67 resolve_dimension(
68 &id,
69 "cpu",
70 &mut container.cpu,
71 choices.and_then(|choices| choices.cpu.as_ref()),
72 cpu.as_deref(),
73 parse_positive_cpu,
74 )?;
75 resolve_dimension(
76 &id,
77 "memory",
78 &mut container.memory,
79 choices.and_then(|choices| choices.memory.as_ref()),
80 memory,
81 parse_positive_memory,
82 )?;
83 }
84 Ok(())
85}
86
87pub fn installed_volume_size_gib(state: &StackState, id: &str) -> Option<u32> {
90 let storage = state
91 .resources
92 .get(id)?
93 .config
94 .downcast_ref::<Container>()?
95 .persistent_storage
96 .as_ref()?;
97 parse_volume_size_gib(&storage.size).ok()
98}
99
100#[derive(Debug, Clone, PartialEq, Eq)]
102pub struct KeptVolumeSize {
103 pub container_id: String,
105 pub requested_gib: u32,
107 pub kept_gib: u32,
109}
110
111pub fn keep_volume_sizes_over_setup_source(
121 requested: &mut ComputeSettings,
122 installed: Option<&StackState>,
123 current: Option<&ComputeSettings>,
124) -> Vec<KeptVolumeSize> {
125 let mut kept = Vec::new();
126 let mut ids: Vec<_> = requested.containers.keys().cloned().collect();
127 ids.sort();
128 for id in ids {
129 let selection = requested.containers.get_mut(&id).expect("id came from map");
130 let Some(requested_gib) = selection.volume_size_gib else {
131 continue;
132 };
133 let Some(installed_gib) = installed.and_then(|state| installed_volume_size_gib(state, &id))
134 else {
135 continue;
136 };
137 let chosen_gib = current
138 .and_then(|current| current.containers.get(&id))
139 .and_then(|selection| selection.volume_size_gib);
140 let kept_gib = chosen_gib.map_or(installed_gib, |chosen| chosen.max(installed_gib));
141 if requested_gib < kept_gib {
142 selection.volume_size_gib = Some(kept_gib);
143 kept.push(KeptVolumeSize {
144 container_id: id,
145 requested_gib,
146 kept_gib,
147 });
148 }
149 }
150 kept
151}
152
153pub fn retain_installed_volume_sizes(
158 stack: &mut Stack,
159 settings: Option<&ComputeSettings>,
160 installed: &StackState,
161) {
162 for (id, entry) in stack.resources.iter_mut() {
163 if settings
164 .and_then(|settings| settings.containers.get(id))
165 .is_some_and(|selection| selection.volume_size_gib.is_some())
166 {
167 continue;
168 }
169 let Some(storage) = entry
170 .config
171 .downcast_mut::<Container>()
172 .and_then(|container| container.persistent_storage.as_mut())
173 else {
174 continue;
175 };
176 let Some(installed_storage) = installed
177 .resources
178 .get(id)
179 .and_then(|resource| resource.config.downcast_ref::<Container>())
180 .and_then(|container| container.persistent_storage.as_ref())
181 else {
182 continue;
183 };
184 if let (Ok(release_gib), Ok(installed_gib)) = (
185 parse_volume_size_gib(&storage.size),
186 parse_volume_size_gib(&installed_storage.size),
187 ) {
188 if release_gib < installed_gib {
189 storage.size = installed_storage.size.clone();
190 }
191 }
192 }
193}
194
195fn resolve_dimension<T: PartialOrd>(
196 id: &str,
197 dimension: &str,
198 spec: &mut ResourceSpec,
199 choices: Option<&ResourceChoiceRange>,
200 selected: Option<&str>,
201 parse: fn(&str) -> std::result::Result<T, String>,
202) -> Result<(), ErrorData> {
203 let parse_value = |value: &str| parse(value).map_err(|reason| invalid(id, dimension, &reason));
204 let Some(choices) = choices else {
205 if let Some(selected) = selected {
206 if parse_value(selected)? != parse_value(&spec.desired)? {
207 return Err(invalid(
208 id,
209 dimension,
210 "the release declares a fixed allocation",
211 ));
212 }
213 }
214 return Ok(());
215 };
216 let min = parse_value(&choices.min)?;
217 let max = parse_value(&choices.max)?;
218 let default = parse_value(&choices.default)?;
219 if min > max || default < min || default > max {
220 return Err(invalid(
221 id,
222 dimension,
223 "range must satisfy min <= default <= max",
224 ));
225 }
226 let value = selected.unwrap_or(&choices.default);
227 let allocation = parse_value(value)?;
228 if allocation < min || allocation > max {
229 return Err(invalid(
230 id,
231 dimension,
232 &format!(
233 "'{value}' is outside the allowed range {}..{}",
234 choices.min, choices.max,
235 ),
236 ));
237 }
238 spec.min = value.to_string();
240 spec.desired = value.to_string();
241 Ok(())
242}
243
244fn parse_positive_cpu(value: &str) -> std::result::Result<f64, String> {
245 let cpu = parse_cpu(value)?;
246 if !cpu.is_finite() || cpu <= 0.0 {
247 return Err(format!("CPU must be finite and positive, got '{value}'"));
248 }
249 Ok(cpu)
250}
251
252fn parse_positive_memory(value: &str) -> std::result::Result<u64, String> {
253 let numeric = ["Ki", "Mi", "Gi", "Ti"]
256 .iter()
257 .find_map(|suffix| value.strip_suffix(suffix))
258 .ok_or_else(|| format!("Memory must use Ki, Mi, Gi, or Ti, got '{value}'"))?;
259 let amount: f64 = numeric
260 .parse()
261 .map_err(|_| format!("Invalid memory '{value}'"))?;
262 if !amount.is_finite() || amount <= 0.0 {
263 return Err(format!("Memory must be finite and positive, got '{value}'"));
264 }
265 let bytes = parse_memory_bytes(value)?;
266 if bytes == 0 || bytes == u64::MAX {
267 return Err(format!("Memory is outside the supported range: '{value}'"));
268 }
269 Ok(bytes)
270}
271
272pub(crate) fn invalid(id: &str, dimension: &str, reason: &str) -> AlienError<ErrorData> {
273 AlienError::new(ErrorData::ContainerResourceSelectionInvalid {
274 resource_id: id.to_string(),
275 dimension: dimension.to_string(),
276 reason: reason.to_string(),
277 })
278}
279
280pub fn parse_volume_size_gib(value: &str) -> std::result::Result<u32, String> {
283 let value = value.trim();
284 let bytes = if let Some(number) = value.strip_suffix("TB") {
288 parse_memory_bytes(&format!("{number}Gi"))?
289 .checked_mul(1000)
290 .ok_or_else(|| format!("Volume capacity '{value}' is too large"))?
291 } else if let Some(number) = value
292 .strip_suffix("GB")
293 .or_else(|| value.strip_suffix("GiB"))
294 {
295 parse_memory_bytes(&format!("{number}Gi"))?
296 } else if let Some(number) = value.strip_suffix("TiB") {
297 parse_memory_bytes(&format!("{number}Ti"))?
298 } else {
299 parse_memory_bytes(value)?
300 };
301 let gib = bytes.div_ceil(1024 * 1024 * 1024);
302 u32::try_from(gib)
303 .ok()
304 .filter(|n| (1..=65536).contains(n))
305 .ok_or_else(|| format!("Volume capacity '{value}' must be between 1 and 65536 GiB"))
306}