Skip to main content

alien_core/
container_resources.rs

1//! Resolve deployment-time container allocations before planning or provisioning.
2
3use crate::{
4    instance_catalog::{parse_cpu, parse_memory_bytes},
5    ComputeSettings, Container, ErrorData, ResourceChoiceRange, ResourceSpec, Stack,
6};
7use alien_error::{AlienError, Result};
8
9/// Apply and validate selections without changing the release's declared resource ranges.
10/// The same resolver is used by compute planning and deployment preparation.
11pub 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    // Both scheduling reservation and runtime limit reflect the deployer's allocation.
110    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    // Keep the public contract in binary units and reject negative/non-finite values
125    // before the catalog's float-to-integer conversion can saturate them.
126    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}