Skip to main content

ostree_ext/container/
skopeo.rs

1//! Fork skopeo as a subprocess
2
3use super::ImageReference;
4use anyhow::{Context, Result};
5use cap_std_ext::cmdext::{CapStdExtCommandExt, CmdFds};
6use containers_image_proxy::oci_spec::image as oci_image;
7use fn_error_context::context;
8use io_lifetimes::OwnedFd;
9use serde::Deserialize;
10use std::io::Read;
11use std::path::Path;
12use std::process::Stdio;
13use std::str::FromStr;
14use tokio::process::Command;
15
16// See `man containers-policy.json` and
17// https://github.com/containers/image/blob/main/signature/policy_types.go
18// Ideally we add something like `skopeo pull --disallow-insecure-accept-anything`
19// but for now we parse the policy.
20const POLICY_PATH: &str = "/etc/containers/policy.json";
21const INSECURE_ACCEPT_ANYTHING: &str = "insecureAcceptAnything";
22
23#[derive(Deserialize)]
24struct PolicyEntry {
25    #[serde(rename = "type")]
26    ty: String,
27}
28#[derive(Deserialize)]
29struct ContainerPolicy {
30    default: Option<Vec<PolicyEntry>>,
31}
32
33impl ContainerPolicy {
34    fn is_default_insecure(&self) -> bool {
35        if let Some(default) = self.default.as_deref() {
36            match default.split_first() {
37                Some((v, &[])) => v.ty == INSECURE_ACCEPT_ANYTHING,
38                _ => false,
39            }
40        } else {
41            false
42        }
43    }
44}
45
46pub(crate) fn container_policy_is_default_insecure() -> Result<bool> {
47    let r = std::io::BufReader::new(std::fs::File::open(POLICY_PATH)?);
48    let policy: ContainerPolicy = serde_json::from_reader(r)?;
49    Ok(policy.is_default_insecure())
50}
51
52/// Create a Command builder for skopeo.
53pub(crate) fn new_cmd() -> std::process::Command {
54    let mut cmd = std::process::Command::new(bootc_utils::skopeo_bin());
55    cmd.stdin(Stdio::null());
56    cmd
57}
58
59/// Spawn the child process
60pub(crate) fn spawn(mut cmd: Command) -> Result<tokio::process::Child> {
61    let cmd = cmd.stdin(Stdio::null()).stderr(Stdio::piped());
62    cmd.spawn().context("Failed to exec skopeo")
63}
64
65/// Use skopeo to copy a container image.
66#[context("Skopeo copy")]
67pub async fn copy(
68    src: &ImageReference,
69    dest: &ImageReference,
70    authfile: Option<&Path>,
71    add_fd: Option<(std::sync::Arc<OwnedFd>, i32)>,
72    progress: bool,
73) -> Result<oci_image::Digest> {
74    let digestfile = tempfile::NamedTempFile::new()?;
75    let mut cmd = new_cmd();
76    cmd.arg("copy");
77    if !progress {
78        cmd.stdout(std::process::Stdio::null());
79    }
80    cmd.arg("--digestfile");
81    cmd.arg(digestfile.path());
82    if let Some((add_fd, n)) = add_fd {
83        let mut fds = CmdFds::new();
84        fds.take_fd_n(add_fd, n);
85        cmd.take_fds(fds);
86    }
87    if let Some(authfile) = authfile {
88        cmd.arg("--authfile");
89        cmd.arg(authfile);
90    }
91    cmd.args(&[src.to_string(), dest.to_string()]);
92    let mut cmd = tokio::process::Command::from(cmd);
93    cmd.kill_on_drop(true);
94    let proc = super::skopeo::spawn(cmd)?;
95    let output = proc.wait_with_output().await?;
96    if !output.status.success() {
97        let stderr = String::from_utf8_lossy(&output.stderr);
98        return Err(anyhow::anyhow!("skopeo failed: {}\n", stderr));
99    }
100    let mut digestfile = digestfile.into_file();
101    let mut r = String::new();
102    digestfile.read_to_string(&mut r)?;
103    Ok(oci_image::Digest::from_str(r.trim())?)
104}
105
106#[cfg(test)]
107mod tests {
108    use super::*;
109
110    // Default value as of the Fedora 34 containers-common-1-21.fc34.noarch package.
111    const DEFAULT_POLICY: &str = indoc::indoc! {r#"
112    {
113        "default": [
114            {
115                "type": "insecureAcceptAnything"
116            }
117        ],
118        "transports":
119            {
120                "docker-daemon":
121                    {
122                        "": [{"type":"insecureAcceptAnything"}]
123                    }
124            }
125    }
126    "#};
127
128    // Stripped down copy from the manual.
129    const REASONABLY_LOCKED_DOWN: &str = indoc::indoc! { r#"
130    {
131        "default": [{"type": "reject"}],
132        "transports": {
133            "dir": {
134                "": [{"type": "insecureAcceptAnything"}]
135            },
136            "atomic": {
137                "hostname:5000/myns/official": [
138                    {
139                        "type": "signedBy",
140                        "keyType": "GPGKeys",
141                        "keyPath": "/path/to/official-pubkey.gpg"
142                    }
143                ]
144            }
145        }
146    }
147    "#};
148
149    #[test]
150    fn policy_is_insecure() {
151        let p: ContainerPolicy = serde_json::from_str(DEFAULT_POLICY).unwrap();
152        assert!(p.is_default_insecure());
153        for &v in &["{}", REASONABLY_LOCKED_DOWN] {
154            let p: ContainerPolicy = serde_json::from_str(v).unwrap();
155            assert!(!p.is_default_insecure());
156        }
157    }
158}