start_command/
container_removal.rs1use std::process::Command;
14
15use chrono::Utc;
16
17use crate::docker_cleanup::docker_command;
18use crate::execution_store::{ExecutionRecord, ExecutionStatus, ExecutionStore};
19
20pub const CONTAINER_STATE_REMOVED: &str = "removed";
22
23pub const CONTAINER_REMOVED_ENV: &str = "START_INTERNAL_CONTAINER_REMOVED";
26
27pub fn is_docker_not_found(success: bool, stderr: &str) -> bool {
33 if success {
34 return false;
35 }
36 let stderr = stderr.to_ascii_lowercase();
37 stderr.contains("no such object") || stderr.contains("no such container")
38}
39
40fn docker_object_missing(target: &str) -> bool {
42 Command::new(docker_command())
43 .args(["inspect", "-f", "{{.Id}}", target])
44 .output()
45 .is_ok_and(|output| {
46 is_docker_not_found(
47 output.status.success(),
48 &String::from_utf8_lossy(&output.stderr),
49 )
50 })
51}
52
53pub fn mark_container_removed(record: &mut ExecutionRecord, observed_at: Option<&str>) {
56 record.container_state = Some(CONTAINER_STATE_REMOVED.to_string());
57 if record.container_removed_observed_at.is_none() {
58 record.container_removed_observed_at = Some(
59 observed_at
60 .map(str::to_string)
61 .unwrap_or_else(|| Utc::now().to_rfc3339()),
62 );
63 }
64}
65
66fn option_str<'a>(record: &'a ExecutionRecord, key: &str) -> Option<&'a str> {
67 record
68 .options
69 .get(key)
70 .and_then(|value| value.as_str())
71 .filter(|value| !value.is_empty())
72}
73
74fn should_probe_removal(record: &ExecutionRecord) -> bool {
78 record.status == ExecutionStatus::Executed
79 && option_str(record, "isolated") == Some("docker")
80 && option_str(record, "isolationMode") == Some("detached")
81 && (option_str(record, "sessionName").is_some()
82 || option_str(record, "containerId").is_some())
83 && record.container_state.as_deref() != Some(CONTAINER_STATE_REMOVED)
84}
85
86pub fn attach_container_removal(mut record: ExecutionRecord) -> ExecutionRecord {
90 if !should_probe_removal(&record) {
91 return record;
92 }
93 let target = option_str(&record, "containerId")
94 .or_else(|| option_str(&record, "sessionName"))
95 .map(str::to_string);
96 if target.is_some_and(|target| docker_object_missing(&target)) {
97 mark_container_removed(&mut record, None);
98 }
99 record
100}
101
102pub fn persist_container_removal(
105 store: &ExecutionStore,
106 stored: &ExecutionRecord,
107 enriched: &ExecutionRecord,
108) {
109 if enriched.container_state.as_deref() != Some(CONTAINER_STATE_REMOVED)
110 || stored.container_state.as_deref() == Some(CONTAINER_STATE_REMOVED)
111 {
112 return;
113 }
114 if let Some(observed_at) = enriched.container_removed_observed_at.as_deref() {
115 let _ = store.mark_container_removed(&stored.uuid, observed_at);
117 }
118}