1use crate::errors::*;
2use serde::{Deserialize, Serialize};
3use std::ffi::OsStr;
4use std::fmt;
5use std::future::{self, Future};
6use std::io::Read;
7use std::process::Stdio;
8use std::str::FromStr;
9use tokio::io::AsyncWriteExt;
10use tokio::process::Command;
11use tokio::signal;
12
13#[derive(Debug, PartialEq, Clone)]
14pub struct ImageRef {
15 pub repo: String,
16 pub tag: Option<String>,
17 pub digest: Option<String>,
18}
19
20impl FromStr for ImageRef {
21 type Err = Error;
22
23 fn from_str(s: &str) -> Result<Self> {
24 if let Some((repo, digest)) = s.split_once('@') {
25 Ok(ImageRef {
26 repo: repo.to_string(),
27 tag: None,
28 digest: Some(digest.to_string()),
29 })
30 } else if let Some((repo, tag)) = s.split_once(':') {
31 Ok(ImageRef {
32 repo: repo.to_string(),
33 tag: Some(tag.to_string()),
34 digest: None,
35 })
36 } else {
37 Ok(ImageRef {
38 repo: s.to_string(),
39 tag: None,
40 digest: None,
41 })
42 }
43 }
44}
45
46impl fmt::Display for ImageRef {
47 fn fmt(&self, w: &mut fmt::Formatter) -> fmt::Result {
48 let repo = &self.repo;
49 if let Some(digest) = &self.digest {
50 write!(w, "{repo}@{digest}")
51 } else if let Some(tag) = &self.tag {
52 write!(w, "{repo}:{tag}")
53 } else {
54 write!(w, "{repo}")
55 }
56 }
57}
58
59#[derive(Debug, Default)]
60pub struct ExecConfig {
61 pub capture_stdout: bool,
62 pub silence_stderr: bool,
63 pub stdin: Option<Vec<u8>>,
64}
65
66pub async fn podman<I, S>(args: I, config: &ExecConfig) -> Result<Vec<u8>>
67where
68 I: IntoIterator<Item = S>,
69 S: AsRef<OsStr> + fmt::Debug,
70{
71 let mut cmd = Command::new("podman");
72 let args = args.into_iter().collect::<Vec<_>>();
73 cmd.args(&args);
74 if config.stdin.is_some() {
75 cmd.stdin(Stdio::piped());
76 }
77 if config.capture_stdout {
78 cmd.stdout(Stdio::piped());
79 }
80 if config.silence_stderr {
81 cmd.stderr(Stdio::null());
82 }
83 debug!("Spawning child process: podman {:?}", args);
84 let mut child = cmd.spawn().context("Failed to execute podman binary")?;
85
86 if let Some(buf) = &config.stdin {
88 if let Some(mut stdin) = child.stdin.take() {
89 stdin.write_all(buf).await?;
90 }
91 }
92
93 let out = child.wait_with_output().await?;
95 debug!("Podman command exited: {:?}", out.status);
96 if !out.status.success() {
97 bail!(
98 "Podman command ({:?}) failed to execute: {:?}",
99 args,
100 out.status
101 );
102 }
103 Ok(out.stdout)
104}
105
106pub async fn pull(image: &str) -> Result<()> {
107 podman(&["image", "pull", "--", image], &ExecConfig::default()).await?;
108 Ok(())
109}
110
111#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
112#[serde(rename_all = "PascalCase")]
113pub struct Image {
114 pub digest: String,
115}
116
117pub async fn inspect(image: &str) -> Result<Image> {
118 let inspect = podman(
119 &["image", "inspect", "--", image],
120 &ExecConfig {
121 capture_stdout: true,
122 silence_stderr: true,
123 ..Default::default()
124 },
125 )
126 .await?;
127 let mut list = serde_json::from_slice::<Vec<Image>>(&inspect)?;
128 debug!("Image inspect result: {list:?}");
129
130 let inspect = list
131 .pop()
132 .with_context(|| anyhow!("Could not find any matching image: {image:?}"))?;
133
134 match list.len() {
135 0 => Ok(inspect),
136 len => bail!(
137 "The specified image is not canonical, inspect returned {}, expected 1",
138 len + 1
139 ),
140 }
141}
142
143#[derive(Debug)]
144pub struct Config<'a> {
145 pub mounts: &'a [(String, String)],
146 pub expose_fuse: bool,
147}
148
149#[derive(Debug, Default)]
150pub struct Exec<'a> {
151 pub capture_stdout: bool,
152 pub cwd: Option<&'a str>,
153 pub user: Option<&'a str>,
154 pub env: &'a [String],
155}
156
157#[derive(Debug)]
158pub struct Container {
159 pub id: String,
160}
161
162impl Container {
163 pub async fn create(image: &str, config: Config<'_>) -> Result<Container> {
164 let mut podman_args = vec![
165 "container".to_string(),
166 "run".to_string(),
167 "--detach".to_string(),
168 "--rm".to_string(),
169 "--network=host".to_string(),
170 "-v=/usr/bin/catatonit:/__:ro".to_string(),
171 "--entrypoint=/__".to_string(),
172 ];
173
174 for (src, dest) in config.mounts {
175 podman_args.push(format!("-v={src}:{dest}"));
176 }
177
178 if config.expose_fuse {
179 debug!("Mapping /dev/fuse into the container");
180 podman_args.push("--device=/dev/fuse".to_string());
181 }
182
183 podman_args.extend(["--".to_string(), image.to_string(), "-P".to_string()]);
184
185 debug!("Creating container...");
186 let mut out = podman(
187 &podman_args,
188 &ExecConfig {
189 capture_stdout: true,
190 ..Default::default()
191 },
192 )
193 .await?;
194 if let Some(idx) = memchr::memchr(b'\n', &out) {
195 out.truncate(idx);
196 }
197 let id = String::from_utf8(out)?;
198 Ok(Container { id })
199 }
200
201 pub async fn exec<I, S>(&self, args: I, options: Exec<'_>) -> Result<Vec<u8>>
202 where
203 I: IntoIterator<Item = S>,
204 S: AsRef<str> + fmt::Debug + Clone,
205 {
206 let args = args.into_iter().collect::<Vec<_>>();
207 let mut a = vec!["container".to_string(), "exec".to_string()];
208
209 if let Some(cwd) = options.cwd {
210 a.extend(["-w".to_string(), cwd.to_string()]);
211 }
212
213 if let Some(user) = options.user {
214 a.extend(["-u".to_string(), user.to_string()]);
215 }
216
217 for env in options.env {
218 a.extend(["-e".to_string(), env.to_string()]);
219 }
220
221 a.extend(["--".to_string(), self.id.to_string()]);
222 a.extend(args.iter().map(|x| x.as_ref().to_string()));
223 let buf = podman(
224 &a,
225 &ExecConfig {
226 capture_stdout: options.capture_stdout,
227 ..Default::default()
228 },
229 )
230 .await
231 .with_context(|| anyhow!("Failed to execute in container: {:?}", args))?;
232 Ok(buf)
233 }
234
235 pub async fn tar(&self, path: &str) -> Result<Vec<u8>> {
236 let a = vec![
237 "container".to_string(),
238 "cp".to_string(),
239 "--".to_string(),
240 format!("{}:{}", self.id, path),
241 "-".to_string(),
242 ];
243 let buf = podman(
244 &a,
245 &ExecConfig {
246 capture_stdout: true,
247 ..Default::default()
248 },
249 )
250 .await
251 .with_context(|| anyhow!("Failed to read from container: {:?}", path))?;
252
253 Ok(buf)
254 }
255
256 pub async fn cat(&self, path: &str) -> Result<Vec<u8>> {
257 let buf = self.tar(path).await?;
258
259 let mut tar = tar::Archive::new(&buf[..]);
260 let mut entries = tar.entries()?;
261 let entry = entries
262 .next()
263 .context("Tar archive generated by podman cp is empty")?;
264 let mut entry = entry?;
265
266 let entry_type = entry.header().entry_type();
267 if entry_type != tar::EntryType::Regular {
268 bail!("Extracted file is not of type file: {entry_type:?}");
269 }
270
271 let mut buf = Vec::new();
272 entry.read_to_end(&mut buf)?;
273
274 Ok(buf)
275 }
276
277 pub async fn write_file(&self, directory: &str, filename: &str, content: &[u8]) -> Result<()> {
278 let mut tar = tar::Builder::new(Vec::new());
280
281 let mut header = tar::Header::new_gnu();
282 header.set_size(content.len() as u64);
283 header.set_mode(0o640);
284
285 debug!(
286 "Adding to archive: {:?} ({} bytes)",
287 filename,
288 content.len()
289 );
290 tar.append_data(&mut header, filename, content)?;
291 let buf = tar.into_inner()?;
292
293 let a = vec![
295 "container".to_string(),
296 "cp".to_string(),
297 "--".to_string(),
298 "-".to_string(),
299 format!("{}:{}", self.id, directory),
300 ];
301 podman(
302 &a,
303 &ExecConfig {
304 stdin: Some(buf),
305 ..Default::default()
306 },
307 )
308 .await
309 .with_context(|| {
310 anyhow!("Failed to write container (directory={directory:?}, filename={filename:?}")
311 })?;
312
313 Ok(())
314 }
315
316 pub async fn kill(&self) -> Result<()> {
317 podman(
318 &["container", "kill", &self.id],
319 &ExecConfig {
320 capture_stdout: true,
321 ..Default::default()
322 },
323 )
324 .await
325 .context("Failed to remove container")?;
326 Ok(())
327 }
328
329 pub async fn run<F: Future<Output = Result<()>>>(&self, fut: F, keep: bool) -> Result<()> {
330 let fut = async {
331 fut.await?;
332 if keep {
333 info!("Keeping container around until ^C...");
334 future::pending().await
335 } else {
336 Ok(())
337 }
338 };
339 let result = tokio::select! {
340 result = fut => result,
341 _ = signal::ctrl_c() => Err(anyhow!("Ctrl-c received")),
342 };
343 debug!("Removing container...");
344 if let Err(err) = self.kill().await {
345 warn!("Failed to kill container {:?}: {:#}", self.id, err);
346 }
347 debug!("Container cleanup complete");
348 result
349 }
350}
351
352#[cfg(target_os = "linux")]
353pub fn test_userns_clone() -> Result<()> {
354 use nix::sched::CloneFlags;
355 use nix::sys::wait::{WaitPidFlag, WaitStatus};
356
357 let cb = Box::new(|| 0);
358 let stack = &mut [0; 1024];
359 let flags = CloneFlags::CLONE_NEWNS | CloneFlags::CLONE_NEWUSER;
360
361 let pid = unsafe { nix::sched::clone(cb, stack, flags, None) }
362 .context("Failed to create user namespace")?;
363 let status = nix::sys::wait::waitpid(pid, Some(WaitPidFlag::__WCLONE))
364 .context("Failed to reap child")?;
365
366 if status != WaitStatus::Exited(pid, 0) {
367 bail!("Unexpected wait result: {:?}", status);
368 }
369
370 Ok(())
371}
372
373#[cfg(target_os = "linux")]
374pub async fn test_for_unprivileged_userns_clone() -> Result<()> {
375 if std::env::var("REPRO_ENV_SKIP_CLONE_CHECK")
376 .map(|x| x != "0")
377 .unwrap_or(false)
378 {
379 debug!("Skipping test if user namespaces can be created");
380 return Ok(());
381 }
382
383 debug!("Testing if user namespaces can be created");
384 if let Err(err) = test_userns_clone() {
385 match tokio::fs::read("/proc/sys/kernel/unprivileged_userns_clone").await {
386 Ok(buf) => {
387 if buf == b"0\n" {
388 warn!("User namespaces are not enabled in /proc/sys/kernel/unprivileged_userns_clone")
389 }
390 }
391 Err(err) => warn!(
392 "Failed to check if unprivileged_userns_clone are allowed: {:#}",
393 err
394 ),
395 }
396
397 Err(err)
398 } else {
399 debug!("Successfully tested for user namespaces");
400 Ok(())
401 }
402}
403
404#[cfg(not(target_os = "linux"))]
405pub async fn test_for_unprivileged_userns_clone() -> Result<()> {
406 Ok(())
407}
408
409#[cfg(test)]
410mod tests {
411 use super::*;
412
413 #[test]
414 fn test_parse_image_ref() -> Result<()> {
415 let image_ref = ImageRef::from_str("rust")?;
416 assert_eq!(
417 image_ref,
418 ImageRef {
419 repo: "rust".to_string(),
420 tag: None,
421 digest: None,
422 }
423 );
424 Ok(())
425 }
426
427 #[test]
428 fn test_parse_image_ref_digest() -> Result<()> {
429 let image_ref = ImageRef::from_str(
430 "rust@sha256:28ee8822965a932e229599b59928f8c2655b2a198af30568acf63e8aff0e8a3a",
431 )?;
432 assert_eq!(
433 image_ref,
434 ImageRef {
435 repo: "rust".to_string(),
436 tag: None,
437 digest: Some(
438 "sha256:28ee8822965a932e229599b59928f8c2655b2a198af30568acf63e8aff0e8a3a"
439 .to_string()
440 ),
441 }
442 );
443 Ok(())
444 }
445
446 #[test]
447 fn test_parse_image_ref_tag() -> Result<()> {
448 let image_ref = ImageRef::from_str("rust:1-alpine3.18")?;
449 assert_eq!(
450 image_ref,
451 ImageRef {
452 repo: "rust".to_string(),
453 tag: Some("1-alpine3.18".to_string()),
454 digest: None,
455 }
456 );
457 Ok(())
458 }
459}