systemprompt_cloud/
docker.rs1use std::io;
15use std::path::{Path, PathBuf};
16use std::process::{Command, ExitStatus, Output, Stdio};
17
18use crate::error::{CloudError, CloudResult};
19
20#[derive(Debug, Clone)]
21pub struct CommandSpec {
22 pub program: String,
23 pub args: Vec<String>,
24 pub current_dir: Option<PathBuf>,
25}
26
27impl CommandSpec {
28 pub fn docker<I, S>(args: I) -> Self
29 where
30 I: IntoIterator<Item = S>,
31 S: Into<String>,
32 {
33 Self {
34 program: "docker".to_owned(),
35 args: args.into_iter().map(Into::into).collect(),
36 current_dir: None,
37 }
38 }
39
40 #[must_use]
41 pub fn rendered(&self) -> String {
42 format!("{} {}", self.program, self.args.join(" "))
43 }
44}
45
46pub trait CommandRunner: Send + Sync {
47 fn output(&self, spec: &CommandSpec) -> io::Result<Output>;
48 fn status(&self, spec: &CommandSpec) -> io::Result<ExitStatus>;
49 fn status_with_stdin(&self, spec: &CommandSpec, stdin: &[u8]) -> io::Result<ExitStatus>;
50}
51
52#[derive(Debug, Clone, Copy, Default)]
53pub struct SystemCommandRunner;
54
55impl SystemCommandRunner {
56 fn command(spec: &CommandSpec) -> Command {
57 let mut command = Command::new(&spec.program);
58 command.args(&spec.args);
59 if let Some(dir) = &spec.current_dir {
60 command.current_dir(dir);
61 }
62 command
63 }
64}
65
66impl CommandRunner for SystemCommandRunner {
67 fn output(&self, spec: &CommandSpec) -> io::Result<Output> {
68 Self::command(spec).output()
69 }
70
71 fn status(&self, spec: &CommandSpec) -> io::Result<ExitStatus> {
72 Self::command(spec).status()
73 }
74
75 fn status_with_stdin(&self, spec: &CommandSpec, stdin: &[u8]) -> io::Result<ExitStatus> {
76 let mut command = Self::command(spec);
77 command.stdin(Stdio::piped());
78 let mut child = command.spawn()?;
79 if let Some(mut handle) = child.stdin.take() {
80 io::Write::write_all(&mut handle, stdin)?;
81 }
82 child.wait()
83 }
84}
85
86pub struct DockerCli {
87 runner: Box<dyn CommandRunner>,
88}
89
90impl std::fmt::Debug for DockerCli {
91 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
92 f.debug_struct("DockerCli").finish_non_exhaustive()
93 }
94}
95
96impl Default for DockerCli {
97 fn default() -> Self {
98 Self::new()
99 }
100}
101
102impl DockerCli {
103 #[must_use]
104 pub fn new() -> Self {
105 Self {
106 runner: Box::new(SystemCommandRunner),
107 }
108 }
109
110 #[must_use]
111 pub const fn with_runner(runner: Box<dyn CommandRunner>) -> Self {
112 Self { runner }
113 }
114
115 pub fn build_image(
116 &self,
117 context_dir: &Path,
118 dockerfile: &Path,
119 image: &str,
120 ) -> CloudResult<()> {
121 let dockerfile_arg = dockerfile.to_string_lossy().into_owned();
122 let mut spec = CommandSpec::docker([
123 "build",
124 "--no-cache",
125 "-f",
126 dockerfile_arg.as_str(),
127 "-t",
128 image,
129 ".",
130 ]);
131 spec.current_dir = Some(context_dir.to_path_buf());
132
133 let status = self.runner.status(&spec).map_err(|e| {
134 CloudError::docker_with(format!("Failed to run: {}", spec.rendered()), e)
135 })?;
136
137 if !status.success() {
138 return Err(CloudError::docker(format!(
139 "Command failed: {}",
140 spec.rendered()
141 )));
142 }
143
144 Ok(())
145 }
146
147 pub fn login(&self, registry: &str, username: &str, token: &str) -> CloudResult<()> {
148 let spec = CommandSpec::docker(["login", registry, "-u", username, "--password-stdin"]);
149
150 let status = self
151 .runner
152 .status_with_stdin(&spec, token.as_bytes())
153 .map_err(|e| {
154 CloudError::docker_with(format!("failed to spawn `docker login {registry}`"), e)
155 })?;
156
157 if !status.success() {
158 return Err(CloudError::docker("Docker login failed"));
159 }
160
161 Ok(())
162 }
163
164 pub fn push(&self, image: &str) -> CloudResult<()> {
165 let spec = CommandSpec::docker(["push", image]);
166
167 let status = self.runner.status(&spec).map_err(|e| {
168 CloudError::docker_with(format!("failed to spawn `docker push {image}`"), e)
169 })?;
170
171 if !status.success() {
172 return Err(CloudError::docker(format!(
173 "Docker push failed for image: {}",
174 image
175 )));
176 }
177
178 Ok(())
179 }
180
181 pub fn output(&self, args: &[&str]) -> io::Result<Output> {
182 self.runner
183 .output(&CommandSpec::docker(args.iter().copied()))
184 }
185
186 pub fn status(&self, args: &[&str]) -> io::Result<ExitStatus> {
187 self.runner
188 .status(&CommandSpec::docker(args.iter().copied()))
189 }
190}