Skip to main content

start_command/
isolation_metadata.rs

1//! Metadata helpers for isolated executions.
2//!
3//! Builds the human-readable `[Isolation]` status lines and the execution
4//! record options map that describe how an isolated command was launched,
5//! including the configurable Docker runtime options (volumes, mounts,
6//! environment variables, privileged mode). Kept separate from `isolation`
7//! so the runtime backends and the metadata representation can evolve
8//! independently.
9
10use crate::args_parser::WrapperOptions;
11use std::collections::HashMap;
12
13/// Build the human-readable `[Isolation]` status lines for docker runtime
14/// options (volumes, mounts, env, privileged). Used for the start block and
15/// log header; empty collections contribute no lines.
16pub fn docker_runtime_status_lines(
17    volumes: &[String],
18    mounts: &[String],
19    env: &[String],
20    privileged: bool,
21    network: Option<&str>,
22    networks: &[String],
23    network_aliases: &[String],
24) -> Vec<String> {
25    let mut lines = Vec::new();
26    if !volumes.is_empty() {
27        lines.push(format!("[Isolation] Volumes: {}", volumes.join(", ")));
28    }
29    if !mounts.is_empty() {
30        lines.push(format!("[Isolation] Mounts: {}", mounts.join(", ")));
31    }
32    if !env.is_empty() {
33        lines.push(format!("[Isolation] Env: {}", env.join(", ")));
34    }
35    if privileged {
36        lines.push("[Isolation] Privileged: true".to_string());
37    }
38    let resolved_networks = if networks.is_empty() {
39        network.into_iter().collect::<Vec<_>>()
40    } else {
41        networks.iter().map(String::as_str).collect::<Vec<_>>()
42    };
43    if let Some(first_network) = resolved_networks.first() {
44        lines.push(format!("[Isolation] Network: {}", first_network));
45    }
46    if resolved_networks.len() > 1 {
47        lines.push(format!(
48            "[Isolation] Networks: {}",
49            resolved_networks.join(", ")
50        ));
51    }
52    if !network_aliases.is_empty() {
53        lines.push(format!(
54            "[Isolation] Network aliases: {}",
55            network_aliases.join(", ")
56        ));
57    }
58    lines
59}
60
61/// Build Docker runtime status lines directly from parsed wrapper options.
62pub fn docker_runtime_status_lines_for_options(options: &WrapperOptions) -> Vec<String> {
63    let mut lines = docker_runtime_status_lines(
64        &options.volumes,
65        &options.mounts,
66        &options.env,
67        options.privileged,
68        options.network.as_deref(),
69        &options.networks,
70        &options.network_aliases,
71    );
72    lines.extend(recovery_status_lines(
73        options.on_kill_resume,
74        options.recovery_command.as_deref(),
75    ));
76    lines
77}
78
79/// `[Isolation]` line announcing launch-time kill recovery (issue #176).
80/// A recovery command on its own implies one attempt.
81pub fn recovery_status_lines(
82    on_kill_resume: Option<u32>,
83    recovery_command: Option<&str>,
84) -> Vec<String> {
85    let Some(count) = effective_on_kill_resume(on_kill_resume, recovery_command) else {
86        return Vec::new();
87    };
88    vec![format!(
89        "[Isolation] On kill: resume up to {} time(s) with {}",
90        count,
91        recovery_command.unwrap_or("the original command")
92    )]
93}
94
95fn effective_on_kill_resume(
96    on_kill_resume: Option<u32>,
97    recovery_command: Option<&str>,
98) -> Option<u32> {
99    on_kill_resume
100        .filter(|count| *count > 0)
101        .or_else(|| recovery_command.map(|_| 1))
102}
103
104/// Execution-record metadata for launch-time kill recovery (issue #176).
105pub fn recovery_metadata(
106    on_kill_resume: Option<u32>,
107    recovery_command: Option<&str>,
108) -> Vec<(String, serde_json::Value)> {
109    let mut entries = Vec::new();
110    if let Some(count) = effective_on_kill_resume(on_kill_resume, recovery_command) {
111        entries.push(("onKillResume".to_string(), serde_json::json!(count)));
112    }
113    if let Some(command) = recovery_command {
114        entries.push(("recoveryCommand".to_string(), serde_json::json!(command)));
115    }
116    entries
117}
118
119/// Build the execution-record metadata entries for docker runtime options.
120/// Returns `(key, value)` pairs to merge into the options map; empty
121/// collections and a false `privileged` flag contribute no entries.
122pub fn docker_runtime_metadata(
123    volumes: &[String],
124    mounts: &[String],
125    env: &[String],
126    privileged: bool,
127    network: Option<&str>,
128    networks: &[String],
129    network_aliases: &[String],
130) -> Vec<(String, serde_json::Value)> {
131    let arr = |items: &[String]| {
132        serde_json::Value::Array(
133            items
134                .iter()
135                .map(|s| serde_json::Value::String(s.clone()))
136                .collect(),
137        )
138    };
139    let mut entries = Vec::new();
140    if !volumes.is_empty() {
141        entries.push(("volumes".to_string(), arr(volumes)));
142    }
143    if !mounts.is_empty() {
144        entries.push(("mounts".to_string(), arr(mounts)));
145    }
146    if !env.is_empty() {
147        entries.push(("env".to_string(), arr(env)));
148    }
149    if privileged {
150        entries.push(("privileged".to_string(), serde_json::Value::Bool(true)));
151    }
152    let resolved_networks = if networks.is_empty() {
153        network.into_iter().collect::<Vec<_>>()
154    } else {
155        networks.iter().map(String::as_str).collect::<Vec<_>>()
156    };
157    if let Some(network) = resolved_networks.first() {
158        entries.push((
159            "network".to_string(),
160            serde_json::Value::String((*network).to_string()),
161        ));
162        entries.push((
163            "networks".to_string(),
164            serde_json::Value::Array(
165                resolved_networks
166                    .iter()
167                    .map(|value| serde_json::Value::String((*value).to_string()))
168                    .collect(),
169            ),
170        ));
171    }
172    if !network_aliases.is_empty() {
173        entries.push(("networkAliases".to_string(), arr(network_aliases)));
174    }
175    entries
176}
177
178/// Build the execution-record options map describing how an isolated command
179/// was launched (environment, mode, session, image, docker runtime options,
180/// endpoint, user, keep-alive). Used to persist the execution record so it can
181/// be surfaced via `--status`/`--list`.
182pub fn build_isolation_options_map(
183    environment: Option<&str>,
184    mode: &str,
185    session_name: &str,
186    effective_image: Option<&str>,
187    options: &WrapperOptions,
188    created_user: Option<&str>,
189) -> HashMap<String, serde_json::Value> {
190    let str_val = |s: &str| serde_json::Value::String(s.to_string());
191    let mut opts_map = HashMap::new();
192    if let Some(env) = environment {
193        opts_map.insert("isolated".to_string(), str_val(env));
194    }
195    opts_map.insert("isolationMode".to_string(), str_val(mode));
196    opts_map.insert("sessionName".to_string(), str_val(session_name));
197    if let Some(v) = effective_image {
198        opts_map.insert("image".to_string(), str_val(v));
199    }
200    for (k, v) in docker_runtime_metadata(
201        &options.volumes,
202        &options.mounts,
203        &options.env,
204        options.privileged,
205        options.network.as_deref(),
206        &options.networks,
207        &options.network_aliases,
208    )
209    .into_iter()
210    .chain(recovery_metadata(
211        options.on_kill_resume,
212        options.recovery_command.as_deref(),
213    )) {
214        opts_map.insert(k, v);
215    }
216    if let Some(v) = &options.endpoint {
217        opts_map.insert("endpoint".to_string(), str_val(v));
218    }
219    if let Some(v) = created_user {
220        opts_map.insert("user".to_string(), str_val(v));
221    }
222    opts_map.insert(
223        "keepAlive".to_string(),
224        serde_json::Value::Bool(options.keep_alive),
225    );
226    opts_map.insert(
227        "autoRemoveDockerContainer".to_string(),
228        serde_json::Value::Bool(options.auto_remove_docker_container),
229    );
230    opts_map.insert(
231        "alwaysCleanupContainer".to_string(),
232        serde_json::Value::Bool(options.always_cleanup_container),
233    );
234    opts_map.insert(
235        "keepContainer".to_string(),
236        serde_json::Value::Bool(options.keep_container),
237    );
238    opts_map.insert(
239        "keepContainerOnFail".to_string(),
240        serde_json::Value::Bool(options.keep_container_on_fail),
241    );
242    opts_map
243}
244
245#[cfg(test)]
246#[path = "isolation_metadata_cases.rs"]
247mod tests;