system_harness/
container.rs

1use crate::{Error, ErrorKind, Status, SystemHarness, SystemTerminal};
2use serde::{Deserialize, Serialize};
3use std::io::{Read, Write};
4use std::process::{Command, Output, Stdio, Child};
5
6fn strip_last_newline(input: &str) -> &str {
7    input
8        .strip_suffix("\r\n")
9        .or(input.strip_suffix("\n"))
10        .unwrap_or(input)
11}
12
13/// Process output to result
14fn output_to_result(output: Output) -> Result<String, Error> {
15    match output.status.success() {
16        true => Ok(strip_last_newline(
17                std::str::from_utf8(&output.stdout)?
18        ).to_string()),
19        false => {
20            let error = std::str::from_utf8(&output.stderr)?;
21            Err(Error::new(ErrorKind::HarnessError, error))
22        }
23    }
24}
25
26/// A container system config
27#[derive(Clone, Serialize, Deserialize)]
28pub struct ContainerSystemConfig {
29
30    /// Container runtime
31    tool: String,
32
33    /// Container image
34    image: String,
35
36}
37
38impl ContainerSystemConfig {
39
40    /// Build and run a container based on name
41    pub fn build(&self) -> Result<ContainerSystem, Error> {
42        let id = Command::new(&self.tool)
43            .arg("create")
44            .arg("-t") 
45            .arg(&self.image)
46            .output()
47            .map_err(|err| err.into())
48            .and_then(output_to_result)
49            .map_err(|err| { log::warn!("{err}"); err })?;
50        log::trace!("Created container: {id}");
51
52        Command::new(&self.tool)
53            .stdout(Stdio::null())
54            .arg("start")
55            .arg(&id)
56            .status()?;
57
58        Ok(ContainerSystem {
59            id,
60            tool: self.tool.clone()
61        })
62    }
63
64}
65
66pub struct ContainerSystem {
67    tool: String,
68    id: String,
69}
70
71pub struct ContainerSystemTerminal {
72    process: Child
73}
74
75#[derive(Deserialize)]
76#[serde(rename_all = "PascalCase")]
77struct State {
78    running: bool,
79    paused: bool
80}
81
82#[derive(Deserialize)]
83#[serde(rename_all = "PascalCase")]
84struct Inspect {
85    state: State
86}
87
88impl SystemTerminal for ContainerSystemTerminal {
89
90    fn send_key(&mut self, _key: crate::Key) -> Result<(), Error> {
91        Err(Error::new(ErrorKind::HarnessError, "Sending a keystroke not supported"))
92    }
93
94}
95
96impl Read for ContainerSystemTerminal {
97    fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
98        self.process.stdout.as_mut()
99            .ok_or(std::io::Error::new(
100                    std::io::ErrorKind::BrokenPipe, 
101                    "Can't read from container"
102                    ))
103            .and_then(|stdout| stdout.read(buf))
104    }
105}
106
107impl Write for ContainerSystemTerminal {
108    fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
109        self.process.stdin.as_mut()
110            .ok_or(std::io::Error::new(
111                    std::io::ErrorKind::BrokenPipe, 
112                    "Can't write to container"
113                    ))
114            .and_then(|stdin| stdin.write(buf))
115    }
116
117    fn flush(&mut self) -> std::io::Result<()> {
118        self.process.stdin.as_mut()
119            .ok_or(std::io::Error::new(
120                    std::io::ErrorKind::BrokenPipe, 
121                    "Can't write to container"
122                    ))
123            .and_then(|stdin| stdin.flush())
124    }
125}
126
127
128
129impl SystemHarness for ContainerSystem {
130
131    type Terminal = ContainerSystemTerminal;
132
133    fn terminal(&self) -> Result<Self::Terminal, Error> {
134        let process = Command::new(&self.tool)
135            .stdin(Stdio::piped())
136            .stdout(Stdio::piped())
137            .arg("exec")
138            .arg("-it")
139            .arg(&self.id)
140            .arg("sh")
141            .spawn()?;
142        Ok(Self::Terminal {
143            process
144        })
145    }
146
147    fn pause(&mut self) -> Result<(), Error> {
148        log::trace!("Pausing container: {}", &self.id); 
149        Command::new(&self.tool)
150            .arg("pause")
151            .arg(&self.id)
152            .output()
153            .map_err(|err| err.into())
154            .and_then(output_to_result)
155            .map(|_| log::trace!("Paused container: {}", self.id))
156    }
157
158    fn resume(&mut self) -> Result<(), Error> {
159        log::trace!("Resuming container: {}", &self.id); 
160        Command::new(&self.tool)
161            .arg("unpause")
162            .arg(&self.id)
163            .output()
164            .map_err(|err| err.into())
165            .and_then(output_to_result)
166            .map(|_| log::trace!("Resumed container: {}", self.id))
167    }
168
169    fn shutdown(&mut self) -> Result<(), Error> {
170        log::trace!("Shutting down container: {}", &self.id); 
171         Command::new(&self.tool)
172            .arg("stop")
173            .arg(&self.id)
174            .output()
175            .map_err(|err| err.into())
176            .and_then(output_to_result)
177            .map(|_| log::trace!("Stopped container: {}", self.id))
178    }
179
180    fn status(&mut self) -> Result<Status, Error> {
181        Command::new(&self.tool)
182            .arg("inspect")
183            .arg(&self.id)
184            .output()
185            .map_err(|err| err.into())
186            .and_then(output_to_result)
187            .map_err(|err| { log::warn!("{err}"); err })
188            .and_then(|stdout| {
189                let inspect: Vec<Inspect> = serde_json::from_str(&stdout)?;
190                inspect.into_iter()
191                    .next()
192                    .ok_or(Error::new(ErrorKind::HarnessError, "Container doesn't exist"))
193                    .and_then(|inspect| {
194                        let state = &inspect.state;
195                        if state.running {
196                            Ok(Status::Running)
197                        } else if state.paused {
198                            Ok(Status::Paused)
199                        } else if !state.running && !state.paused {
200                            Ok(Status::Shutdown)
201                        } else {
202                            Err(Error::new(ErrorKind::HarnessError,
203                                    format!("Unhandled status")))
204                        }
205                    })
206            })
207    }
208
209    fn running(&mut self) -> Result<bool, Error> {
210        self.status().map(|status| status == Status::Running)
211    }
212
213}
214
215impl Drop for ContainerSystem {
216    fn drop(&mut self) {
217        if let Ok(running) = self.running() {
218            if running {
219                if let Ok(()) = self.shutdown() {
220                    log::trace!("Deleting container: {}", &self.id); 
221                    let _ = Command::new(&self.tool)
222                        .args(&["rm", "-f", &self.id])
223                        .output();
224                } else {
225                    log::warn!("Failed to shutdown: {}", &self.id);
226                }
227            }
228        }
229    }
230}