Skip to main content

ostree_ext/container/
mod.rs

1//! # APIs bridging OSTree and container images
2//!
3//! This module provides the core infrastructure for bidirectionally mapping between
4//! OCI/Docker container images and OSTree repositories. It enables bootable container
5//! images to be fetched from registries, stored efficiently, and deployed as ostree
6//! commits.
7//!
8//! ## Overview
9//!
10//! Container images are fundamentally layers of tarballs. This module leverages the
11//! [`crate::tar`] module to import container layers as ostree content, and exports
12//! ostree commits back to container images. The key insight is that ostree's
13//! content-addressed object storage maps naturally to OCI layer deduplication.
14//!
15//! When a container image is imported ("pulled"), each layer becomes an ostree commit.
16//! These layer commits are then merged into a single "merge commit" that represents
17//! the complete filesystem state. This merge commit is what gets deployed as a
18//! bootable system.
19//!
20//! ## On-Disk Storage Structure
21//!
22//! Container images are stored in the ostree repository (typically `/sysroot/ostree/repo/`)
23//! using a structured reference (ref) namespace:
24//!
25//! ### Reference Namespace
26//!
27//! - **`ostree/container/blob/<escaped-digest>`**: Each OCI layer is stored as a
28//!   separate ostree commit. The digest (e.g., `sha256:abc123...`) is escaped using
29//!   [`crate::refescape`] to be valid as an ostree ref. For example:
30//!   `ostree/container/blob/sha256_3A_abc123...`
31//!
32//! - **`ostree/container/image/<escaped-image-reference>`**: Points to the "merge
33//!   commit" for a pulled image. The image reference (e.g., `docker://quay.io/org/image:tag`)
34//!   is escaped similarly. This is the ref that deployments point to.
35//!
36//! - **`ostree/container/baseimage/<project>/<index>`**: Used to protect base images
37//!   from garbage collection. Tooling that builds derived images locally should write
38//!   refs under this prefix to prevent the base layers from being pruned.
39//!
40//! ### Layer Storage
41//!
42//! Each container layer is stored as an ostree commit with a special structure:
43//!
44//! - **OSTree "chunk" layers**: Layers that are part of the base ostree commit use
45//!   the "object set" format - the filenames in the commit *are* the object checksums.
46//!   This enables efficient reconstruction of the original ostree commit.
47//!
48//! - **Derived layers**: Non-ostree layers (e.g., from `RUN` commands in a Containerfile)
49//!   are imported as regular filesystem trees and stored as standard ostree commits.
50//!
51//! ### The Merge Commit
52//!
53//! The merge commit (`ostree/container/image/...`) combines all layers into a single
54//! filesystem tree. It contains critical metadata in its commit metadata:
55//!
56//! - `ostree.manifest-digest`: The OCI manifest digest (e.g., `sha256:...`)
57//! - `ostree.manifest`: The complete OCI manifest as JSON
58//! - `ostree.container.image-config`: The OCI image configuration as JSON
59//!
60//! This metadata enables round-tripping: an imported image can be re-exported with
61//! its original manifest structure preserved.
62//!
63//! ## Import Flow
64//!
65//! The import process (implemented in [`store::ImageImporter`]) follows these steps:
66//!
67//! 1. **Manifest fetch**: Contact the registry via containers-image-proxy (skopeo)
68//!    to retrieve the image manifest and configuration.
69//!
70//! 2. **Layout parsing**: Analyze the manifest to identify:
71//!    - The base ostree layer (identified by the `ostree.final-diffid` label)
72//!    - Component/chunk layers (split object sets)
73//!    - Derived layers (non-ostree content)
74//!
75//! 3. **Layer caching check**: For each layer, check if an ostree ref already exists
76//!    for that digest. Cached layers are skipped, enabling efficient incremental updates.
77//!
78//! 4. **Layer import**: For uncached layers:
79//!    - Fetch the compressed tarball from the registry
80//!    - Decompress and parse the tar stream
81//!    - Import content into ostree (handling xattrs via `bare-split-xattrs` format)
82//!    - Create an ostree commit and write the layer ref
83//!
84//! 5. **Merge commit creation**: Overlay all layers (processing OCI whiteout files)
85//!    to create a unified filesystem tree. Apply SELinux labeling if needed.
86//!    Store manifest/config metadata and write the image ref.
87//!
88//! 6. **Garbage collection**: Prune layer refs that are no longer referenced by any
89//!    image or deployment.
90//!
91//! ## Tar Stream Format
92//!
93//! The tar format used for ostree layers is documented in [`crate::tar`]. Key points:
94//!
95//! - Uses `bare-split-xattrs` repository mode to handle extended attributes
96//! - XAttrs are stored in separate `.file-xattrs` objects, avoiding tar xattr complexity
97//! - `/etc` in container images maps to `/usr/etc` in ostree (the "3-way merge" location)
98//! - Hardlinks are used for deduplication within layers
99//!
100//! ## Connection to Deployments
101//!
102//! When bootc deploys an image, it creates an ostree deployment whose "origin" file
103//! references the container image. The origin contains:
104//!
105//! - The [`OstreeImageReference`] specifying the image and signature verification method
106//! - The merge commit checksum
107//!
108//! On subsequent boots, bootc can compare the deployed commit against the registry
109//! manifest to detect available updates.
110//!
111//! ## Signatures
112//!
113//! OSTree supports GPG and ed25519 signatures natively. When fetching container images,
114//! signature verification can be configured via [`SignatureSource`]:
115//!
116//! - `OstreeRemote(name)`: Verify using the named ostree remote's keyring
117//! - `ContainerPolicy`: Defer to containers-policy.json (requires explicit allow)
118//! - `ContainerPolicyAllowInsecure`: Use containers-policy.json defaults (not recommended)
119//!
120//! This library defines a URL-like schema to combine signature verification with
121//! image references:
122//!
123//! - `ostree-remote-registry:<remotename>:<containerimage>` - Verify via ostree remote
124//! - `ostree-image-signed:<transport>:<image>` - Use container policy
125//! - `ostree-unverified-registry:<image>` - No verification (not recommended)
126//!
127//! Example: `ostree-remote-registry:fedora:quay.io/fedora/fedora-bootc:latest`
128//!
129//! See [`OstreeImageReference`] for parsing and generating these strings.
130//!
131//! ## Layering and Derived Images
132//!
133//! Container image layering is fully supported. A typical bootable image structure:
134//!
135//! 1. **Base ostree layer**: Contains the core OS as an ostree commit
136//! 2. **Chunk layers**: Split objects for efficient updates (optional)
137//! 3. **Derived layers**: Additional content from Containerfile `RUN` commands
138//!
139//! The `ostree.final-diffid` label in the image configuration marks where the
140//! ostree content ends and derived content begins. This enables:
141//!
142//! - Efficient layer sharing between images with the same base
143//! - Proper SELinux labeling of derived content using the base policy
144//! - Round-trip export preserving the layer structure
145//!
146//! ## Key Types
147//!
148//! - [`Transport`]: OCI/Docker transport (registry, oci-dir, containers-storage, etc.)
149//! - [`ImageReference`]: Container image reference with transport
150//! - [`OstreeImageReference`]: Image reference plus signature verification method
151//! - [`SignatureSource`]: How to verify image signatures
152//! - [`store::ImageImporter`]: Main import orchestrator
153//! - [`store::PreparedImport`]: Analysis of layers to fetch
154//! - [`store::LayeredImageState`]: State of a pulled image
155//! - [`ManifestDiff`]: Comparison between two image manifests
156//!
157//! ## Submodules
158//!
159//! - [`store`]: Core storage and import logic
160//! - [`deploy`]: Integration with ostree deployments
161//! - [`skopeo`]: Skopeo subprocess management for registry operations
162
163use anyhow::anyhow;
164use cap_std_ext::cap_std;
165use cap_std_ext::cap_std::fs::Dir;
166use containers_image_proxy::oci_spec;
167use ostree::glib;
168use serde::Serialize;
169
170use std::borrow::Cow;
171use std::collections::HashMap;
172use std::fmt::Debug;
173use std::ops::Deref;
174use std::str::FromStr;
175
176/// The label injected into a container image that contains the ostree commit SHA-256.
177pub const OSTREE_COMMIT_LABEL: &str = "ostree.commit";
178
179/// The name of an annotation attached to a layer which names the packages/components
180/// which are part of it.
181pub(crate) const CONTENT_ANNOTATION: &str = "ostree.components";
182/// The character we use to separate values in [`CONTENT_ANNOTATION`].
183pub(crate) const COMPONENT_SEPARATOR: char = ',';
184
185/// Our generic catchall fatal error, expected to be converted
186/// to a string to output to a terminal or logs.
187type Result<T> = anyhow::Result<T>;
188
189/// A backend/transport for OCI/Docker images.
190pub type Transport = containers_image_proxy::transport::Transport;
191
192/// Combination of a remote image reference and transport.
193pub type ImageReference = containers_image_proxy::ImageReference;
194
195/// Policy for signature verification.
196#[derive(Debug, Clone, PartialEq, Eq, Hash)]
197pub enum SignatureSource {
198    /// Fetches will use the named ostree remote for signature verification of the ostree commit.
199    OstreeRemote(String),
200    /// Fetches will defer to the `containers-policy.json`, but we make a best effort to reject `default: insecureAcceptAnything` policy.
201    ContainerPolicy,
202    /// NOT RECOMMENDED.  Fetches will defer to the `containers-policy.json` default which is usually `insecureAcceptAnything`.
203    ContainerPolicyAllowInsecure,
204}
205
206/// A commonly used pre-OCI label for versions.
207pub const LABEL_VERSION: &str = "version";
208
209/// Combination of a signature verification mechanism, and a standard container image reference.
210///
211#[derive(Debug, Clone, PartialEq, Eq, Hash)]
212pub struct OstreeImageReference {
213    /// The signature verification mechanism.
214    pub sigverify: SignatureSource,
215    /// The container image reference.
216    pub imgref: ImageReference,
217}
218
219impl TryFrom<&str> for SignatureSource {
220    type Error = anyhow::Error;
221
222    fn try_from(value: &str) -> Result<Self> {
223        match value {
224            "ostree-image-signed" => Ok(Self::ContainerPolicy),
225            "ostree-unverified-image" => Ok(Self::ContainerPolicyAllowInsecure),
226            o => match o.strip_prefix("ostree-remote-image:") {
227                Some(rest) => Ok(Self::OstreeRemote(rest.to_string())),
228                _ => Err(anyhow!("Invalid signature source: {}", o)),
229            },
230        }
231    }
232}
233
234impl FromStr for SignatureSource {
235    type Err = anyhow::Error;
236
237    fn from_str(s: &str) -> Result<Self> {
238        Self::try_from(s)
239    }
240}
241
242impl TryFrom<&str> for OstreeImageReference {
243    type Error = anyhow::Error;
244
245    fn try_from(value: &str) -> Result<Self> {
246        let (first, second) = value
247            .split_once(':')
248            .ok_or_else(|| anyhow!("Missing ':' in {}", value))?;
249        let (sigverify, rest) = match first {
250            "ostree-image-signed" => (SignatureSource::ContainerPolicy, Cow::Borrowed(second)),
251            "ostree-unverified-image" => (
252                SignatureSource::ContainerPolicyAllowInsecure,
253                Cow::Borrowed(second),
254            ),
255            // Shorthand for ostree-unverified-image:registry:
256            "ostree-unverified-registry" => (
257                SignatureSource::ContainerPolicyAllowInsecure,
258                Cow::Owned(format!("registry:{second}")),
259            ),
260            // This is a shorthand for ostree-remote-image with registry:
261            "ostree-remote-registry" => {
262                let (remote, rest) = second
263                    .split_once(':')
264                    .ok_or_else(|| anyhow!("Missing second ':' in {}", value))?;
265                (
266                    SignatureSource::OstreeRemote(remote.to_string()),
267                    Cow::Owned(format!("registry:{rest}")),
268                )
269            }
270            "ostree-remote-image" => {
271                let (remote, rest) = second
272                    .split_once(':')
273                    .ok_or_else(|| anyhow!("Missing second ':' in {}", value))?;
274                (
275                    SignatureSource::OstreeRemote(remote.to_string()),
276                    Cow::Borrowed(rest),
277                )
278            }
279            o => {
280                return Err(anyhow!("Invalid ostree image reference scheme: {}", o));
281            }
282        };
283        let imgref = rest.deref().try_into()?;
284        Ok(Self { sigverify, imgref })
285    }
286}
287
288impl FromStr for OstreeImageReference {
289    type Err = anyhow::Error;
290
291    fn from_str(s: &str) -> Result<Self> {
292        Self::try_from(s)
293    }
294}
295
296impl std::fmt::Display for SignatureSource {
297    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
298        match self {
299            SignatureSource::OstreeRemote(r) => write!(f, "ostree-remote-image:{r}"),
300            SignatureSource::ContainerPolicy => write!(f, "ostree-image-signed"),
301            SignatureSource::ContainerPolicyAllowInsecure => {
302                write!(f, "ostree-unverified-image")
303            }
304        }
305    }
306}
307
308impl std::fmt::Display for OstreeImageReference {
309    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
310        match (&self.sigverify, &self.imgref) {
311            (SignatureSource::ContainerPolicyAllowInsecure, imgref)
312                if imgref.transport == Transport::Registry =>
313            {
314                // Because allow-insecure is the effective default, allow formatting
315                // without it.  Note this formatting is asymmetric and cannot be
316                // re-parsed.
317                if f.alternate() {
318                    write!(f, "{}", self.imgref)
319                } else {
320                    write!(f, "ostree-unverified-registry:{}", self.imgref.name)
321                }
322            }
323            (sigverify, imgref) => {
324                write!(f, "{sigverify}:{imgref}")
325            }
326        }
327    }
328}
329
330/// Represents the difference in layer/blob content between two OCI image manifests.
331#[derive(Debug, Serialize)]
332pub struct ManifestDiff<'a> {
333    /// The source container image manifest.
334    #[serde(skip)]
335    pub from: &'a oci_spec::image::ImageManifest,
336    /// The target container image manifest.
337    #[serde(skip)]
338    pub to: &'a oci_spec::image::ImageManifest,
339    /// Layers which are present in the old image but not the new image.
340    #[serde(skip)]
341    pub removed: Vec<&'a oci_spec::image::Descriptor>,
342    /// Layers which are present in the new image but not the old image.
343    #[serde(skip)]
344    pub added: Vec<&'a oci_spec::image::Descriptor>,
345    /// Total number of layers
346    pub total: u64,
347    /// Size of total number of layers.
348    pub total_size: u64,
349    /// Number of layers removed
350    pub n_removed: u64,
351    /// Size of the number of layers removed
352    pub removed_size: u64,
353    /// Number of packages added
354    pub n_added: u64,
355    /// Size of the number of layers added
356    pub added_size: u64,
357}
358
359impl<'a> ManifestDiff<'a> {
360    /// Compute the layer difference between two OCI image manifests.
361    pub fn new(
362        src: &'a oci_spec::image::ImageManifest,
363        dest: &'a oci_spec::image::ImageManifest,
364    ) -> Self {
365        let src_layers = src
366            .layers()
367            .iter()
368            .map(|l| (l.digest().digest(), l))
369            .collect::<HashMap<_, _>>();
370        let dest_layers = dest
371            .layers()
372            .iter()
373            .map(|l| (l.digest().digest(), l))
374            .collect::<HashMap<_, _>>();
375        let mut removed = Vec::new();
376        let mut added = Vec::new();
377        for (blobid, &descriptor) in src_layers.iter() {
378            if !dest_layers.contains_key(blobid) {
379                removed.push(descriptor);
380            }
381        }
382        removed.sort_by(|a, b| a.digest().digest().cmp(b.digest().digest()));
383        for (blobid, &descriptor) in dest_layers.iter() {
384            if !src_layers.contains_key(blobid) {
385                added.push(descriptor);
386            }
387        }
388        added.sort_by(|a, b| a.digest().digest().cmp(b.digest().digest()));
389
390        fn layersum<'a, I: Iterator<Item = &'a oci_spec::image::Descriptor>>(layers: I) -> u64 {
391            layers.map(|layer| layer.size()).sum()
392        }
393        let total = dest_layers.len() as u64;
394        let total_size = layersum(dest.layers().iter());
395        let n_removed = removed.len() as u64;
396        let n_added = added.len() as u64;
397        let removed_size = layersum(removed.iter().copied());
398        let added_size = layersum(added.iter().copied());
399        ManifestDiff {
400            from: src,
401            to: dest,
402            removed,
403            added,
404            total,
405            total_size,
406            n_removed,
407            removed_size,
408            n_added,
409            added_size,
410        }
411    }
412}
413
414impl ManifestDiff<'_> {
415    /// Prints the total, removed and added content between two OCI images
416    pub fn print(&self) {
417        let print_total = self.total;
418        let print_total_size = glib::format_size(self.total_size);
419        let print_n_removed = self.n_removed;
420        let print_removed_size = glib::format_size(self.removed_size);
421        let print_n_added = self.n_added;
422        let print_added_size = glib::format_size(self.added_size);
423        println!("Total new layers: {print_total:<4}  Size: {print_total_size}");
424        println!("Removed layers:   {print_n_removed:<4}  Size: {print_removed_size}");
425        println!("Added layers:     {print_n_added:<4}  Size: {print_added_size}");
426    }
427}
428
429/// Apply default configuration for container image pulls to an existing configuration.
430/// For example, if `authfile` is not set, and `auth_anonymous` is `false`, and a global configuration file exists, it will be used.
431///
432/// If there is no configured explicit subprocess for skopeo, and the process is running
433/// as root, then a default isolation of running the process via `nobody` will be applied.
434pub fn merge_default_container_proxy_opts(
435    config: &mut containers_image_proxy::ImageProxyConfig,
436) -> Result<()> {
437    let user = rustix::process::getuid()
438        .is_root()
439        .then_some(isolation::DEFAULT_UNPRIVILEGED_USER);
440    merge_default_container_proxy_opts_with_isolation(config, user)
441}
442
443/// Apply default configuration for container image pulls, with optional support
444/// for isolation as an unprivileged user.
445pub fn merge_default_container_proxy_opts_with_isolation(
446    config: &mut containers_image_proxy::ImageProxyConfig,
447    isolation_user: Option<&str>,
448) -> Result<()> {
449    let auth_specified =
450        config.auth_anonymous || config.authfile.is_some() || config.auth_data.is_some();
451    if !auth_specified {
452        let root = &Dir::open_ambient_dir("/", cap_std::ambient_authority())?;
453        config.auth_data = crate::globals::get_global_authfile(root)?.map(|a| a.1);
454        // If there's no auth data, then force on anonymous pulls to ensure
455        // that the container stack doesn't try to find it in the standard
456        // container paths.
457        if config.auth_data.is_none() {
458            config.auth_anonymous = true;
459        }
460    }
461    // By default, drop privileges, unless the higher level code
462    // has configured the skopeo command explicitly.
463    let isolation_user = config
464        .skopeo_cmd
465        .is_none()
466        .then_some(isolation_user.as_ref())
467        .flatten();
468    if let Some(user) = isolation_user {
469        // Read the default authfile if it exists and pass it via file descriptor
470        // which will ensure it's readable when we drop privileges.
471        if let Some(authfile) = config.authfile.take() {
472            config.auth_data = Some(std::fs::File::open(authfile)?);
473        }
474        let cmd = crate::isolation::unprivileged_subprocess(bootc_utils::skopeo_bin(), user);
475        config.skopeo_cmd = Some(cmd);
476    }
477    Ok(())
478}
479
480/// Convenience helper to return the labels, if present.
481pub(crate) fn labels_of(
482    config: &oci_spec::image::ImageConfiguration,
483) -> Option<&HashMap<String, String>> {
484    config.config().as_ref().and_then(|c| c.labels().as_ref())
485}
486
487/// Retrieve the version number from an image configuration.
488pub fn version_for_config(config: &oci_spec::image::ImageConfiguration) -> Option<&str> {
489    if let Some(labels) = labels_of(config) {
490        for k in [oci_spec::image::ANNOTATION_VERSION, LABEL_VERSION] {
491            if let Some(v) = labels.get(k) {
492                return Some(v.as_str());
493            }
494        }
495    }
496    None
497}
498
499/// Apply appropriate container proxy options based on transport type
500pub fn apply_container_proxy_opts_for_transport(
501    config: &mut containers_image_proxy::ImageProxyConfig,
502    transport: Transport,
503) -> Result<()> {
504    if transport == Transport::ContainerStorage {
505        // Fetching from containers-storage, may require privileges to read files
506        merge_default_container_proxy_opts_with_isolation(config, None)
507    } else {
508        // Apply our defaults to the proxy config
509        merge_default_container_proxy_opts(config)
510    }
511}
512
513pub mod deploy;
514mod encapsulate;
515pub use encapsulate::*;
516mod unencapsulate;
517pub use unencapsulate::*;
518pub mod skopeo;
519pub mod store;
520mod update_detachedmeta;
521pub use update_detachedmeta::*;
522
523use crate::isolation;
524
525#[cfg(test)]
526mod tests {
527    use std::process::Command;
528
529    use containers_image_proxy::ImageProxyConfig;
530
531    use super::*;
532
533    #[test]
534    fn test_serializable_transport() {
535        for v in [
536            Transport::Registry,
537            Transport::ContainerStorage,
538            Transport::OciArchive,
539            Transport::DockerArchive,
540            Transport::OciDir,
541        ] {
542            assert_eq!(Transport::try_from(v.to_string().as_ref()).unwrap(), v);
543        }
544    }
545
546    const INVALID_IRS: &[&str] = &["", "foo://", "docker:blah", "registry:", "foo:bar"];
547    const VALID_IRS: &[&str] = &[
548        "containers-storage:localhost/someimage",
549        "docker://quay.io/exampleos/blah:sometag",
550    ];
551
552    #[test]
553    fn test_imagereference() {
554        let ir: ImageReference = "registry:quay.io/exampleos/blah".try_into().unwrap();
555        assert_eq!(ir.transport, Transport::Registry);
556        assert_eq!(ir.name, "quay.io/exampleos/blah");
557        assert_eq!(ir.to_string(), "docker://quay.io/exampleos/blah");
558
559        for &v in VALID_IRS {
560            ImageReference::try_from(v).unwrap();
561        }
562
563        for &v in INVALID_IRS {
564            if ImageReference::try_from(v).is_ok() {
565                panic!("Should fail to parse: {v}")
566            }
567        }
568        struct Case {
569            s: &'static str,
570            transport: Transport,
571            name: &'static str,
572        }
573        for case in [
574            Case {
575                s: "oci:somedir",
576                transport: Transport::OciDir,
577                name: "somedir",
578            },
579            Case {
580                s: "dir:/some/dir/blah",
581                transport: Transport::Dir,
582                name: "/some/dir/blah",
583            },
584            Case {
585                s: "oci-archive:/path/to/foo.ociarchive",
586                transport: Transport::OciArchive,
587                name: "/path/to/foo.ociarchive",
588            },
589            Case {
590                s: "docker-archive:/path/to/foo.dockerarchive",
591                transport: Transport::DockerArchive,
592                name: "/path/to/foo.dockerarchive",
593            },
594            Case {
595                s: "containers-storage:localhost/someimage:blah",
596                transport: Transport::ContainerStorage,
597                name: "localhost/someimage:blah",
598            },
599        ] {
600            let ir: ImageReference = case.s.try_into().unwrap();
601            assert_eq!(ir.transport, case.transport);
602            assert_eq!(ir.name, case.name);
603            let reserialized = ir.to_string();
604            assert_eq!(case.s, reserialized.as_str());
605        }
606    }
607
608    #[test]
609    fn test_ostreeimagereference() {
610        // Test both long form `ostree-remote-image:$myremote:registry` and the
611        // shorthand `ostree-remote-registry:$myremote`.
612        let ir_s = "ostree-remote-image:myremote:registry:quay.io/exampleos/blah";
613        let ir_registry = "ostree-remote-registry:myremote:quay.io/exampleos/blah";
614        for &ir_s in &[ir_s, ir_registry] {
615            let ir: OstreeImageReference = ir_s.try_into().unwrap();
616            assert_eq!(
617                ir.sigverify,
618                SignatureSource::OstreeRemote("myremote".to_string())
619            );
620            assert_eq!(ir.imgref.transport, Transport::Registry);
621            assert_eq!(ir.imgref.name, "quay.io/exampleos/blah");
622            assert_eq!(
623                ir.to_string(),
624                "ostree-remote-image:myremote:docker://quay.io/exampleos/blah"
625            );
626        }
627
628        // Also verify our FromStr impls
629
630        let ir: OstreeImageReference = ir_s.try_into().unwrap();
631        assert_eq!(ir, OstreeImageReference::from_str(ir_s).unwrap());
632        // test our Eq implementation
633        assert_eq!(&ir, &OstreeImageReference::try_from(ir_registry).unwrap());
634
635        let ir_s = "ostree-image-signed:docker://quay.io/exampleos/blah";
636        let ir: OstreeImageReference = ir_s.try_into().unwrap();
637        assert_eq!(ir.sigverify, SignatureSource::ContainerPolicy);
638        assert_eq!(ir.imgref.transport, Transport::Registry);
639        assert_eq!(ir.imgref.name, "quay.io/exampleos/blah");
640        assert_eq!(ir.to_string(), ir_s);
641        assert_eq!(format!("{:#}", &ir), ir_s);
642
643        let ir_s = "ostree-unverified-image:docker://quay.io/exampleos/blah";
644        let ir: OstreeImageReference = ir_s.try_into().unwrap();
645        assert_eq!(ir.sigverify, SignatureSource::ContainerPolicyAllowInsecure);
646        assert_eq!(ir.imgref.transport, Transport::Registry);
647        assert_eq!(ir.imgref.name, "quay.io/exampleos/blah");
648        assert_eq!(
649            ir.to_string(),
650            "ostree-unverified-registry:quay.io/exampleos/blah"
651        );
652        let ir_shorthand =
653            OstreeImageReference::try_from("ostree-unverified-registry:quay.io/exampleos/blah")
654                .unwrap();
655        assert_eq!(&ir_shorthand, &ir);
656        assert_eq!(format!("{:#}", &ir), "docker://quay.io/exampleos/blah");
657    }
658
659    #[test]
660    fn test_merge_authopts() {
661        // Verify idempotence of authentication processing
662        let mut c = ImageProxyConfig::default();
663        let authf = std::fs::File::open("/dev/null").unwrap();
664        c.auth_data = Some(authf);
665        super::merge_default_container_proxy_opts_with_isolation(&mut c, None).unwrap();
666        assert!(!c.auth_anonymous);
667        assert!(c.authfile.is_none());
668        assert!(c.auth_data.is_some());
669        assert!(c.skopeo_cmd.is_none());
670        super::merge_default_container_proxy_opts_with_isolation(&mut c, None).unwrap();
671        assert!(!c.auth_anonymous);
672        assert!(c.authfile.is_none());
673        assert!(c.auth_data.is_some());
674        assert!(c.skopeo_cmd.is_none());
675
676        // Verify interaction with explicit isolation
677        let mut c = ImageProxyConfig::default();
678        c.skopeo_cmd = Some(Command::new("skopeo"));
679        super::merge_default_container_proxy_opts_with_isolation(&mut c, Some("foo")).unwrap();
680        assert_eq!(c.skopeo_cmd.unwrap().get_program(), "skopeo");
681    }
682}