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