xianyun_backends/docker/
mod.rs1use std::sync::Arc;
8
9use xianyun_executor::exec::{CommandRunner, ExecError};
10
11pub trait Backend {
13 fn inspect(&self, container: &str) -> Result<bool, ExecError>;
16
17 fn exec(&self, container: &str, args: &[&str]) -> Result<String, ExecError>;
20
21 fn copy_to_container(
24 &self,
25 container: &str,
26 host_path: &str,
27 container_path: &str,
28 ) -> Result<(), ExecError>;
29}
30
31pub struct Client {
33 executor: Arc<dyn CommandRunner + Send + Sync>,
34}
35
36impl Client {
37 pub fn new(executor: Arc<dyn CommandRunner + Send + Sync>) -> Self {
39 Self { executor }
40 }
41}
42
43impl Backend for Client {
44 fn inspect(&self, container: &str) -> Result<bool, ExecError> {
45 self.executor.check(
46 "docker",
47 &[
48 "inspect".into(),
49 "--format={{.Id}}".into(),
50 container.into(),
51 ],
52 )
53 }
54
55 fn exec(&self, container: &str, args: &[&str]) -> Result<String, ExecError> {
56 let mut exec_args = vec!["exec".to_string(), container.to_string()];
57 exec_args.extend(args.iter().map(std::string::ToString::to_string));
58
59 let out = self.executor.read_stdout("docker", &exec_args)?;
60
61 Ok(out.trim_end_matches('\n').to_string())
62 }
63
64 fn copy_to_container(
65 &self,
66 container: &str,
67 host_path: &str,
68 container_path: &str,
69 ) -> Result<(), ExecError> {
70 let dest = format!("{container}:{container_path}");
71
72 self.executor.exec_streamed_command(
73 None,
74 "docker",
75 &["cp".to_string(), host_path.to_string(), dest],
76 )
77 }
78}
79
80#[cfg(test)]
81mod tests {
82 use super::*;
83 use xianyun_testutil::MockCommandRunner;
84
85 #[test]
88 fn inspect_returns_true_when_container_exists() {
89 let mock = MockCommandRunner::new();
90 mock.expect_check(
91 "docker",
92 &["inspect", "--format={{.Id}}", "my-container"],
93 Ok(true),
94 );
95
96 let client = Client::new(Arc::new(mock));
97
98 let ok = client.inspect("my-container").unwrap();
99 assert!(ok);
100 }
101
102 #[test]
103 fn exec_runs_docker_exec_and_returns_trimmed_stdout() {
104 let mock = MockCommandRunner::new();
105 mock.expect_read_stdout(
106 "docker",
107 &["exec", "my-container", "cat", "/etc/hostname"],
108 Ok("my-container\n".to_string()),
109 );
110
111 let client = Client::new(Arc::new(mock));
112
113 let out = client
114 .exec("my-container", &["cat", "/etc/hostname"])
115 .unwrap();
116 assert_eq!(out, "my-container");
117 }
118
119 #[test]
120 fn copy_to_container_runs_docker_cp() {
121 let mock = MockCommandRunner::new();
122 mock.expect_exec_streamed_command(
123 "docker",
124 &["cp", "/host/file.txt", "my-container:/dest/file.txt"],
125 Ok(()),
126 );
127
128 let client = Client::new(Arc::new(mock));
129
130 client
131 .copy_to_container("my-container", "/host/file.txt", "/dest/file.txt")
132 .unwrap();
133 }
134
135 #[test]
136 fn inspect_wraps_execution_errors() {
137 let mock = MockCommandRunner::new();
142 mock.expect_check(
143 "docker",
144 &["inspect", "--format={{.Id}}", "missing-container"],
145 Err(ExecError::RunFailed(
146 "docker daemon unreachable".to_string(),
147 )),
148 );
149
150 let client = Client::new(Arc::new(mock));
151
152 let err = client.inspect("missing-container").unwrap_err();
153 assert!(matches!(err, ExecError::RunFailed(_)));
154 }
155}