1use crate::args_parser::WrapperOptions;
11use crate::recovery_delay::effective_on_kill_resume_delay;
12use std::collections::HashMap;
13
14pub fn docker_attribution_labels(
17 options: &crate::isolation::IsolationOptions,
18 session: &str,
19) -> Vec<String> {
20 let mut labels = options.labels.clone();
21 let mut inferred_root = session;
22 if options.resume_count > 0 {
23 while let Some((prefix, count)) = inferred_root.rsplit_once("-resume-") {
24 if count.is_empty() || !count.bytes().all(|byte| byte.is_ascii_digit()) {
25 break;
26 }
27 inferred_root = prefix;
28 }
29 }
30 let root = options.root_session.as_deref().unwrap_or(inferred_root);
31 let uuid = options
32 .uuid
33 .clone()
34 .or_else(|| options.execution_id.clone())
35 .unwrap_or_else(|| uuid::Uuid::new_v4().to_string());
36 labels.extend([
37 format!("start-command.session={}", session),
38 format!("start-command.root-session={}", root),
39 format!("start-command.uuid={}", uuid),
40 format!("start-command.resume-count={}", options.resume_count),
41 ]);
42 labels
43}
44
45pub fn docker_runtime_status_lines(
49 volumes: &[String],
50 mounts: &[String],
51 env: &[String],
52 privileged: bool,
53 network: Option<&str>,
54 networks: &[String],
55 network_aliases: &[String],
56) -> Vec<String> {
57 let mut lines = Vec::new();
58 if !volumes.is_empty() {
59 lines.push(format!("[Isolation] Volumes: {}", volumes.join(", ")));
60 }
61 if !mounts.is_empty() {
62 lines.push(format!("[Isolation] Mounts: {}", mounts.join(", ")));
63 }
64 if !env.is_empty() {
65 lines.push(format!("[Isolation] Env: {}", env.join(", ")));
66 }
67 if privileged {
68 lines.push("[Isolation] Privileged: true".to_string());
69 }
70 let resolved_networks = if networks.is_empty() {
71 network.into_iter().collect::<Vec<_>>()
72 } else {
73 networks.iter().map(String::as_str).collect::<Vec<_>>()
74 };
75 if let Some(first_network) = resolved_networks.first() {
76 lines.push(format!("[Isolation] Network: {}", first_network));
77 }
78 if resolved_networks.len() > 1 {
79 lines.push(format!(
80 "[Isolation] Networks: {}",
81 resolved_networks.join(", ")
82 ));
83 }
84 if !network_aliases.is_empty() {
85 lines.push(format!(
86 "[Isolation] Network aliases: {}",
87 network_aliases.join(", ")
88 ));
89 }
90 lines
91}
92
93pub fn docker_runtime_status_lines_for_options(options: &WrapperOptions) -> Vec<String> {
95 let mut lines = docker_runtime_status_lines(
96 &options.volumes,
97 &options.mounts,
98 &options.env,
99 options.privileged,
100 options.network.as_deref(),
101 &options.networks,
102 &options.network_aliases,
103 );
104 if options.init == Some(true) {
105 let at = lines
107 .iter()
108 .position(|line| line.starts_with("[Isolation] Network"))
109 .unwrap_or(lines.len());
110 lines.insert(at, "[Isolation] Init: true".to_string());
111 }
112 if !options.labels.is_empty() {
113 lines.push(format!("[Isolation] Labels: {}", options.labels.join(", ")));
114 }
115 if let Some(line) = crate::docker_resource_options::log_line(
116 &crate::docker_resource_options::specs(options),
117 &options.resolved_limits,
118 ) {
119 lines.push(line);
120 }
121 lines.extend(recovery_status_lines_with_delay(
122 options.on_kill_resume,
123 options.recovery_command.as_deref(),
124 options.on_kill_resume_delay.as_deref(),
125 ));
126 lines
127}
128
129pub fn recovery_status_lines(
132 on_kill_resume: Option<u32>,
133 recovery_command: Option<&str>,
134) -> Vec<String> {
135 recovery_status_lines_with_delay(on_kill_resume, recovery_command, None)
136}
137
138pub fn recovery_status_lines_with_delay(
141 on_kill_resume: Option<u32>,
142 recovery_command: Option<&str>,
143 on_kill_resume_delay: Option<&str>,
144) -> Vec<String> {
145 let Some(count) = effective_on_kill_resume(on_kill_resume, recovery_command) else {
146 return Vec::new();
147 };
148 let after = effective_on_kill_resume_delay(on_kill_resume_delay)
149 .map(|delay| format!(" after a random {}s delay", delay))
150 .unwrap_or_default();
151 vec![format!(
152 "[Isolation] On kill: resume up to {} time(s){} with {}",
153 count,
154 after,
155 recovery_command.unwrap_or("the original command")
156 )]
157}
158
159fn effective_on_kill_resume(
160 on_kill_resume: Option<u32>,
161 recovery_command: Option<&str>,
162) -> Option<u32> {
163 on_kill_resume
164 .filter(|count| *count > 0)
165 .or_else(|| recovery_command.map(|_| 1))
166}
167
168pub fn recovery_metadata(
170 on_kill_resume: Option<u32>,
171 recovery_command: Option<&str>,
172) -> Vec<(String, serde_json::Value)> {
173 recovery_metadata_with_delay(on_kill_resume, recovery_command, None)
174}
175
176pub fn recovery_metadata_with_delay(
179 on_kill_resume: Option<u32>,
180 recovery_command: Option<&str>,
181 on_kill_resume_delay: Option<&str>,
182) -> Vec<(String, serde_json::Value)> {
183 let mut entries = Vec::new();
184 let Some(count) = effective_on_kill_resume(on_kill_resume, recovery_command) else {
185 return entries;
186 };
187 entries.push(("onKillResume".to_string(), serde_json::json!(count)));
188 if let Some(command) = recovery_command {
189 entries.push(("recoveryCommand".to_string(), serde_json::json!(command)));
190 }
191 if let Some(delay) = effective_on_kill_resume_delay(on_kill_resume_delay) {
192 entries.push(("onKillResumeDelay".to_string(), serde_json::json!(delay)));
193 }
194 entries
195}
196
197pub fn docker_runtime_metadata(
201 volumes: &[String],
202 mounts: &[String],
203 env: &[String],
204 privileged: bool,
205 network: Option<&str>,
206 networks: &[String],
207 network_aliases: &[String],
208) -> Vec<(String, serde_json::Value)> {
209 let arr = |items: &[String]| {
210 serde_json::Value::Array(
211 items
212 .iter()
213 .map(|s| serde_json::Value::String(s.clone()))
214 .collect(),
215 )
216 };
217 let mut entries = Vec::new();
218 if !volumes.is_empty() {
219 entries.push(("volumes".to_string(), arr(volumes)));
220 }
221 if !mounts.is_empty() {
222 entries.push(("mounts".to_string(), arr(mounts)));
223 }
224 if !env.is_empty() {
225 entries.push(("env".to_string(), arr(env)));
226 }
227 if privileged {
228 entries.push(("privileged".to_string(), serde_json::Value::Bool(true)));
229 }
230 let resolved_networks = if networks.is_empty() {
231 network.into_iter().collect::<Vec<_>>()
232 } else {
233 networks.iter().map(String::as_str).collect::<Vec<_>>()
234 };
235 if let Some(network) = resolved_networks.first() {
236 entries.push((
237 "network".to_string(),
238 serde_json::Value::String((*network).to_string()),
239 ));
240 entries.push((
241 "networks".to_string(),
242 serde_json::Value::Array(
243 resolved_networks
244 .iter()
245 .map(|value| serde_json::Value::String((*value).to_string()))
246 .collect(),
247 ),
248 ));
249 }
250 if !network_aliases.is_empty() {
251 entries.push(("networkAliases".to_string(), arr(network_aliases)));
252 }
253 entries
254}
255
256pub fn build_isolation_options_map(
261 environment: Option<&str>,
262 mode: &str,
263 session_name: &str,
264 effective_image: Option<&str>,
265 options: &WrapperOptions,
266 created_user: Option<&str>,
267) -> HashMap<String, serde_json::Value> {
268 let str_val = |s: &str| serde_json::Value::String(s.to_string());
269 let mut opts_map = HashMap::new();
270 if !options.labels.is_empty() {
271 opts_map.insert("labels".to_string(), serde_json::json!(options.labels));
272 }
273 if environment == Some("docker") && mode == "detached" {
274 opts_map.insert("commandHandoff".to_string(), serde_json::json!(true));
275 }
276 if let Some(env) = environment {
277 opts_map.insert("isolated".to_string(), str_val(env));
278 }
279 opts_map.insert("isolationMode".to_string(), str_val(mode));
280 opts_map.insert("sessionName".to_string(), str_val(session_name));
281 if let Some(v) = effective_image {
282 opts_map.insert("image".to_string(), str_val(v));
283 }
284 for (k, v) in docker_runtime_metadata(
285 &options.volumes,
286 &options.mounts,
287 &options.env,
288 options.privileged,
289 options.network.as_deref(),
290 &options.networks,
291 &options.network_aliases,
292 )
293 .into_iter()
294 .chain(recovery_metadata_with_delay(
295 options.on_kill_resume,
296 options.recovery_command.as_deref(),
297 options.on_kill_resume_delay.as_deref(),
298 )) {
299 opts_map.insert(k, v);
300 }
301 if options.init == Some(true) {
302 opts_map.insert("init".into(), serde_json::Value::Bool(true));
303 }
304 if let Some(config) = &options.cpu_penalty_config {
305 opts_map.insert("cpuPenaltyConfig".into(), serde_json::json!(config));
306 opts_map.insert(
307 "baseResourceLimits".into(),
308 serde_json::json!(options.resource_limits),
309 );
310 }
311 opts_map.insert(
312 "resourceLimits".into(),
313 serde_json::json!(options.resource_limits),
314 );
315 opts_map.insert("resolvedLimits".into(), options.resolved_limits.clone());
316 opts_map.insert(
317 "resourceLimitSpecs".into(),
318 crate::docker_resource_options::specs(options),
319 );
320 if let Some(value) = &options.on_kill_resume_memory {
321 opts_map.insert("onKillResumeMemory".into(), serde_json::json!(value));
322 }
323 if let Some(v) = &options.endpoint {
324 opts_map.insert("endpoint".to_string(), str_val(v));
325 }
326 if let Some(v) = created_user {
327 opts_map.insert("user".to_string(), str_val(v));
328 }
329 opts_map.insert(
330 "keepAlive".to_string(),
331 serde_json::Value::Bool(options.keep_alive),
332 );
333 opts_map.insert(
334 "autoRemoveDockerContainer".to_string(),
335 serde_json::Value::Bool(options.auto_remove_docker_container),
336 );
337 opts_map.insert(
338 "alwaysCleanupContainer".to_string(),
339 serde_json::Value::Bool(options.always_cleanup_container),
340 );
341 opts_map.insert(
342 "keepContainer".to_string(),
343 serde_json::Value::Bool(options.keep_container),
344 );
345 opts_map.insert(
346 "keepContainerOnFail".to_string(),
347 serde_json::Value::Bool(options.keep_container_on_fail),
348 );
349 opts_map
350}
351
352#[cfg(test)]
353#[path = "isolation_metadata_cases.rs"]
354mod tests;