start_command/
docker_resource_limits.rs1use serde_json::Value;
15
16use crate::docker_cleanup::docker_command;
17use crate::execution_control::CommandRunner;
18
19pub const DEFAULT_SHM_SIZE: i64 = 64 * 1024 * 1024;
21
22const KIB: i64 = 1024;
23const MIB: i64 = KIB * 1024;
24const GIB: i64 = MIB * 1024;
25
26pub fn format_docker_bytes(bytes: i64) -> String {
29 for (unit, size) in [("g", GIB), ("m", MIB), ("k", KIB)] {
30 if bytes >= size && bytes % size == 0 {
31 return format!("{}{}", bytes / size, unit);
32 }
33 }
34 bytes.to_string()
35}
36
37pub fn format_docker_cpus(nano_cpus: i64) -> String {
39 let text = format!("{:.9}", nano_cpus as f64 / 1e9);
40 let text = text.trim_end_matches('0').trim_end_matches('.');
41 text.to_string()
42}
43
44fn number(value: Option<&Value>) -> Option<i64> {
45 match value? {
46 Value::Number(n) => n.as_i64().or_else(|| n.as_f64().map(|f| f as i64)),
47 Value::String(s) => s.trim().parse().ok(),
48 _ => None,
49 }
50}
51
52fn positive(value: Option<&Value>) -> Option<i64> {
53 number(value).filter(|n| *n > 0)
54}
55
56fn non_empty(value: Option<&Value>) -> Option<String> {
57 value
58 .and_then(Value::as_str)
59 .map(str::trim)
60 .filter(|s| !s.is_empty())
61 .map(str::to_string)
62}
63
64fn scalar_text(value: Option<&Value>) -> String {
65 match value {
66 Some(Value::String(s)) => s.clone(),
67 Some(Value::Number(n)) => n.to_string(),
68 Some(Value::Bool(b)) => b.to_string(),
69 _ => String::new(),
70 }
71}
72
73pub fn parse_docker_resource_limits(host_config: &Value) -> Vec<String> {
79 let Some(h) = host_config.as_object() else {
80 return Vec::new();
81 };
82 let mut limits = Vec::new();
83
84 if let Some(memory) = positive(h.get("Memory")) {
85 limits.push(format!("--memory={}", format_docker_bytes(memory)));
86 match number(h.get("MemorySwap")) {
87 Some(-1) => limits.push("--memory-swap=-1".to_string()),
88 Some(swap) if swap > 0 => {
89 limits.push(format!("--memory-swap={}", format_docker_bytes(swap)))
90 }
91 _ => {}
92 }
93 }
94 if let Some(reservation) = positive(h.get("MemoryReservation")) {
95 limits.push(format!(
96 "--memory-reservation={}",
97 format_docker_bytes(reservation)
98 ));
99 }
100
101 if let Some(nano_cpus) = positive(h.get("NanoCpus")) {
102 limits.push(format!("--cpus={}", format_docker_cpus(nano_cpus)));
103 } else {
104 if let Some(quota) = positive(h.get("CpuQuota")) {
105 limits.push(format!("--cpu-quota={}", quota));
106 }
107 if let Some(period) = positive(h.get("CpuPeriod")) {
108 limits.push(format!("--cpu-period={}", period));
109 }
110 }
111 if let Some(shares) = positive(h.get("CpuShares")) {
112 limits.push(format!("--cpu-shares={}", shares));
113 }
114 if let Some(cpus) = non_empty(h.get("CpusetCpus")) {
115 limits.push(format!("--cpuset-cpus={}", cpus));
116 }
117 if let Some(mems) = non_empty(h.get("CpusetMems")) {
118 limits.push(format!("--cpuset-mems={}", mems));
119 }
120
121 if let Some(pids) = positive(h.get("PidsLimit")) {
122 limits.push(format!("--pids-limit={}", pids));
123 }
124
125 if let Some(shm) = positive(h.get("ShmSize")) {
126 if shm != DEFAULT_SHM_SIZE {
127 limits.push(format!("--shm-size={}", format_docker_bytes(shm)));
128 }
129 }
130
131 if let Some(storage) = h.get("StorageOpt").and_then(Value::as_object) {
132 let mut keys: Vec<&String> = storage.keys().collect();
133 keys.sort();
134 for key in keys {
135 let value = scalar_text(storage.get(key));
136 if !value.trim().is_empty() {
137 limits.push(format!("--storage-opt={}={}", key, value.trim()));
138 }
139 }
140 }
141
142 if let Some(ulimits) = h.get("Ulimits").and_then(Value::as_array) {
143 for ulimit in ulimits {
144 if let Some(name) = non_empty(ulimit.get("Name")) {
145 limits.push(format!(
146 "--ulimit={}={}:{}",
147 name,
148 scalar_text(ulimit.get("Soft")),
149 scalar_text(ulimit.get("Hard"))
150 ));
151 }
152 }
153 }
154
155 limits
156}
157
158pub fn read_docker_resource_limits<R: CommandRunner + ?Sized>(
162 container_name: &str,
163 runner: &R,
164) -> Option<Vec<String>> {
165 let docker = docker_command().to_string_lossy().to_string();
166 let args: Vec<String> = [
167 "inspect",
168 "--format",
169 "{{json .HostConfig}}",
170 container_name,
171 ]
172 .iter()
173 .map(|s| s.to_string())
174 .collect();
175 let result = runner.run(&docker, &args);
176 if !result.success {
177 return None;
178 }
179 let host_config: Value = serde_json::from_str(result.stdout.trim()).ok()?;
180 Some(parse_docker_resource_limits(&host_config))
181}
182
183pub fn normalize_resource_limits(value: Option<&Value>) -> Vec<String> {
186 match value {
187 Some(Value::Array(items)) => items
188 .iter()
189 .map(|item| match item {
190 Value::String(s) => s.clone(),
191 other => other.to_string(),
192 })
193 .filter(|flag| flag.starts_with("--"))
194 .collect(),
195 Some(Value::String(text)) => text
196 .split_whitespace()
197 .filter(|flag| flag.starts_with("--"))
198 .map(str::to_string)
199 .collect(),
200 _ => Vec::new(),
201 }
202}
203
204pub fn build_resource_limits_status_line(limits: &[String]) -> Option<String> {
206 if limits.is_empty() {
207 None
208 } else {
209 Some(format!("[Isolation] Resource limits: {}", limits.join(" ")))
210 }
211}