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    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}
73
74/// Build the execution-record metadata entries for docker runtime options.
75/// Returns `(key, value)` pairs to merge into the options map; empty
76/// collections and a false `privileged` flag contribute no entries.
77pub fn docker_runtime_metadata(
78    volumes: &[String],
79    mounts: &[String],
80    env: &[String],
81    privileged: bool,
82    network: Option<&str>,
83    networks: &[String],
84    network_aliases: &[String],
85) -> Vec<(String, serde_json::Value)> {
86    let arr = |items: &[String]| {
87        serde_json::Value::Array(
88            items
89                .iter()
90                .map(|s| serde_json::Value::String(s.clone()))
91                .collect(),
92        )
93    };
94    let mut entries = Vec::new();
95    if !volumes.is_empty() {
96        entries.push(("volumes".to_string(), arr(volumes)));
97    }
98    if !mounts.is_empty() {
99        entries.push(("mounts".to_string(), arr(mounts)));
100    }
101    if !env.is_empty() {
102        entries.push(("env".to_string(), arr(env)));
103    }
104    if privileged {
105        entries.push(("privileged".to_string(), serde_json::Value::Bool(true)));
106    }
107    let resolved_networks = if networks.is_empty() {
108        network.into_iter().collect::<Vec<_>>()
109    } else {
110        networks.iter().map(String::as_str).collect::<Vec<_>>()
111    };
112    if let Some(network) = resolved_networks.first() {
113        entries.push((
114            "network".to_string(),
115            serde_json::Value::String((*network).to_string()),
116        ));
117        entries.push((
118            "networks".to_string(),
119            serde_json::Value::Array(
120                resolved_networks
121                    .iter()
122                    .map(|value| serde_json::Value::String((*value).to_string()))
123                    .collect(),
124            ),
125        ));
126    }
127    if !network_aliases.is_empty() {
128        entries.push(("networkAliases".to_string(), arr(network_aliases)));
129    }
130    entries
131}
132
133/// Build the execution-record options map describing how an isolated command
134/// was launched (environment, mode, session, image, docker runtime options,
135/// endpoint, user, keep-alive). Used to persist the execution record so it can
136/// be surfaced via `--status`/`--list`.
137pub fn build_isolation_options_map(
138    environment: Option<&str>,
139    mode: &str,
140    session_name: &str,
141    effective_image: Option<&str>,
142    options: &WrapperOptions,
143    created_user: Option<&str>,
144) -> HashMap<String, serde_json::Value> {
145    let str_val = |s: &str| serde_json::Value::String(s.to_string());
146    let mut opts_map = HashMap::new();
147    if let Some(env) = environment {
148        opts_map.insert("isolated".to_string(), str_val(env));
149    }
150    opts_map.insert("isolationMode".to_string(), str_val(mode));
151    opts_map.insert("sessionName".to_string(), str_val(session_name));
152    if let Some(v) = effective_image {
153        opts_map.insert("image".to_string(), str_val(v));
154    }
155    for (k, v) in docker_runtime_metadata(
156        &options.volumes,
157        &options.mounts,
158        &options.env,
159        options.privileged,
160        options.network.as_deref(),
161        &options.networks,
162        &options.network_aliases,
163    ) {
164        opts_map.insert(k, v);
165    }
166    if let Some(v) = &options.endpoint {
167        opts_map.insert("endpoint".to_string(), str_val(v));
168    }
169    if let Some(v) = created_user {
170        opts_map.insert("user".to_string(), str_val(v));
171    }
172    opts_map.insert(
173        "keepAlive".to_string(),
174        serde_json::Value::Bool(options.keep_alive),
175    );
176    opts_map.insert(
177        "autoRemoveDockerContainer".to_string(),
178        serde_json::Value::Bool(options.auto_remove_docker_container),
179    );
180    opts_map.insert(
181        "alwaysCleanupContainer".to_string(),
182        serde_json::Value::Bool(options.always_cleanup_container),
183    );
184    opts_map.insert(
185        "keepContainer".to_string(),
186        serde_json::Value::Bool(options.keep_container),
187    );
188    opts_map.insert(
189        "keepContainerOnFail".to_string(),
190        serde_json::Value::Bool(options.keep_container_on_fail),
191    );
192    opts_map
193}
194
195#[cfg(test)]
196#[path = "isolation_metadata_cases.rs"]
197mod tests;