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.
190#[derive(Copy, Clone, Hash, Debug, PartialEq, Eq)]
191pub enum Transport {
192    /// A remote Docker/OCI registry (`registry:` or `docker://`)
193    Registry,
194    /// A local OCI directory (`oci:`)
195    OciDir,
196    /// A local OCI archive tarball (`oci-archive:`)
197    OciArchive,
198    /// A local Docker archive tarball (`docker-archive:`)
199    DockerArchive,
200    /// Local container storage (`containers-storage:`)
201    ContainerStorage,
202    /// Local directory (`dir:`)
203    Dir,
204    /// Local Docker daemon (`docker-daemon:`)
205    DockerDaemon,
206}
207
208/// Combination of a remote image reference and transport.
209///
210/// For example,
211#[derive(Debug, Clone, Hash, PartialEq, Eq)]
212pub struct ImageReference {
213    /// The storage and transport for the image
214    pub transport: Transport,
215    /// The image name (e.g. `quay.io/somerepo/someimage:latest`)
216    pub name: String,
217}
218
219/// Policy for signature verification.
220#[derive(Debug, Clone, PartialEq, Eq, Hash)]
221pub enum SignatureSource {
222    /// Fetches will use the named ostree remote for signature verification of the ostree commit.
223    OstreeRemote(String),
224    /// Fetches will defer to the `containers-policy.json`, but we make a best effort to reject `default: insecureAcceptAnything` policy.
225    ContainerPolicy,
226    /// NOT RECOMMENDED.  Fetches will defer to the `containers-policy.json` default which is usually `insecureAcceptAnything`.
227    ContainerPolicyAllowInsecure,
228}
229
230/// A commonly used pre-OCI label for versions.
231pub const LABEL_VERSION: &str = "version";
232
233/// Combination of a signature verification mechanism, and a standard container image reference.
234///
235#[derive(Debug, Clone, PartialEq, Eq, Hash)]
236pub struct OstreeImageReference {
237    /// The signature verification mechanism.
238    pub sigverify: SignatureSource,
239    /// The container image reference.
240    pub imgref: ImageReference,
241}
242
243impl TryFrom<&str> for Transport {
244    type Error = anyhow::Error;
245
246    fn try_from(value: &str) -> Result<Self> {
247        Ok(match value {
248            Self::REGISTRY_STR | "docker" => Self::Registry,
249            Self::OCI_STR => Self::OciDir,
250            Self::OCI_ARCHIVE_STR => Self::OciArchive,
251            Self::DOCKER_ARCHIVE_STR => Self::DockerArchive,
252            Self::CONTAINERS_STORAGE_STR => Self::ContainerStorage,
253            Self::LOCAL_DIRECTORY_STR => Self::Dir,
254            Self::DOCKER_DAEMON_STR => Self::DockerDaemon,
255            o => return Err(anyhow!("Unknown transport '{}'", o)),
256        })
257    }
258}
259
260impl Transport {
261    const OCI_STR: &'static str = "oci";
262    const OCI_ARCHIVE_STR: &'static str = "oci-archive";
263    const DOCKER_ARCHIVE_STR: &'static str = "docker-archive";
264    const CONTAINERS_STORAGE_STR: &'static str = "containers-storage";
265    const LOCAL_DIRECTORY_STR: &'static str = "dir";
266    const REGISTRY_STR: &'static str = "registry";
267    const DOCKER_DAEMON_STR: &'static str = "docker-daemon";
268
269    /// Retrieve an identifier that can then be re-parsed from [`Transport::try_from::<&str>`].
270    pub fn serializable_name(&self) -> &'static str {
271        match self {
272            Transport::Registry => Self::REGISTRY_STR,
273            Transport::OciDir => Self::OCI_STR,
274            Transport::OciArchive => Self::OCI_ARCHIVE_STR,
275            Transport::DockerArchive => Self::DOCKER_ARCHIVE_STR,
276            Transport::ContainerStorage => Self::CONTAINERS_STORAGE_STR,
277            Transport::Dir => Self::LOCAL_DIRECTORY_STR,
278            Transport::DockerDaemon => Self::DOCKER_DAEMON_STR,
279        }
280    }
281}
282
283impl TryFrom<&str> for ImageReference {
284    type Error = anyhow::Error;
285
286    fn try_from(value: &str) -> Result<Self> {
287        let (transport_name, mut name) = value
288            .split_once(':')
289            .ok_or_else(|| anyhow!("Missing ':' in {}", value))?;
290        let transport: Transport = transport_name.try_into()?;
291        if name.is_empty() {
292            return Err(anyhow!("Invalid empty name in {}", value));
293        }
294        if transport_name == "docker" {
295            name = name
296                .strip_prefix("//")
297                .ok_or_else(|| anyhow!("Missing // in docker:// in {}", value))?;
298        }
299        Ok(Self {
300            transport,
301            name: name.to_string(),
302        })
303    }
304}
305
306impl FromStr for ImageReference {
307    type Err = anyhow::Error;
308
309    fn from_str(s: &str) -> Result<Self> {
310        Self::try_from(s)
311    }
312}
313
314impl TryFrom<&str> for SignatureSource {
315    type Error = anyhow::Error;
316
317    fn try_from(value: &str) -> Result<Self> {
318        match value {
319            "ostree-image-signed" => Ok(Self::ContainerPolicy),
320            "ostree-unverified-image" => Ok(Self::ContainerPolicyAllowInsecure),
321            o => match o.strip_prefix("ostree-remote-image:") {
322                Some(rest) => Ok(Self::OstreeRemote(rest.to_string())),
323                _ => Err(anyhow!("Invalid signature source: {}", o)),
324            },
325        }
326    }
327}
328
329impl FromStr for SignatureSource {
330    type Err = anyhow::Error;
331
332    fn from_str(s: &str) -> Result<Self> {
333        Self::try_from(s)
334    }
335}
336
337impl TryFrom<&str> for OstreeImageReference {
338    type Error = anyhow::Error;
339
340    fn try_from(value: &str) -> Result<Self> {
341        let (first, second) = value
342            .split_once(':')
343            .ok_or_else(|| anyhow!("Missing ':' in {}", value))?;
344        let (sigverify, rest) = match first {
345            "ostree-image-signed" => (SignatureSource::ContainerPolicy, Cow::Borrowed(second)),
346            "ostree-unverified-image" => (
347                SignatureSource::ContainerPolicyAllowInsecure,
348                Cow::Borrowed(second),
349            ),
350            // Shorthand for ostree-unverified-image:registry:
351            "ostree-unverified-registry" => (
352                SignatureSource::ContainerPolicyAllowInsecure,
353                Cow::Owned(format!("registry:{second}")),
354            ),
355            // This is a shorthand for ostree-remote-image with registry:
356            "ostree-remote-registry" => {
357                let (remote, rest) = second
358                    .split_once(':')
359                    .ok_or_else(|| anyhow!("Missing second ':' in {}", value))?;
360                (
361                    SignatureSource::OstreeRemote(remote.to_string()),
362                    Cow::Owned(format!("registry:{rest}")),
363                )
364            }
365            "ostree-remote-image" => {
366                let (remote, rest) = second
367                    .split_once(':')
368                    .ok_or_else(|| anyhow!("Missing second ':' in {}", value))?;
369                (
370                    SignatureSource::OstreeRemote(remote.to_string()),
371                    Cow::Borrowed(rest),
372                )
373            }
374            o => {
375                return Err(anyhow!("Invalid ostree image reference scheme: {}", o));
376            }
377        };
378        let imgref = rest.deref().try_into()?;
379        Ok(Self { sigverify, imgref })
380    }
381}
382
383impl FromStr for OstreeImageReference {
384    type Err = anyhow::Error;
385
386    fn from_str(s: &str) -> Result<Self> {
387        Self::try_from(s)
388    }
389}
390
391impl std::fmt::Display for Transport {
392    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
393        let s = match self {
394            // TODO once skopeo supports this, canonicalize as registry:
395            Self::Registry => "docker://",
396            Self::OciArchive => "oci-archive:",
397            Self::DockerArchive => "docker-archive:",
398            Self::OciDir => "oci:",
399            Self::ContainerStorage => "containers-storage:",
400            Self::Dir => "dir:",
401            Self::DockerDaemon => "docker-daemon:",
402        };
403        f.write_str(s)
404    }
405}
406
407impl std::fmt::Display for ImageReference {
408    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
409        write!(f, "{}{}", self.transport, self.name)
410    }
411}
412
413impl std::fmt::Display for SignatureSource {
414    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
415        match self {
416            SignatureSource::OstreeRemote(r) => write!(f, "ostree-remote-image:{r}"),
417            SignatureSource::ContainerPolicy => write!(f, "ostree-image-signed"),
418            SignatureSource::ContainerPolicyAllowInsecure => {
419                write!(f, "ostree-unverified-image")
420            }
421        }
422    }
423}
424
425impl std::fmt::Display for OstreeImageReference {
426    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
427        match (&self.sigverify, &self.imgref) {
428            (SignatureSource::ContainerPolicyAllowInsecure, imgref)
429                if imgref.transport == Transport::Registry =>
430            {
431                // Because allow-insecure is the effective default, allow formatting
432                // without it.  Note this formatting is asymmetric and cannot be
433                // re-parsed.
434                if f.alternate() {
435                    write!(f, "{}", self.imgref)
436                } else {
437                    write!(f, "ostree-unverified-registry:{}", self.imgref.name)
438                }
439            }
440            (sigverify, imgref) => {
441                write!(f, "{sigverify}:{imgref}")
442            }
443        }
444    }
445}
446
447/// Represents the difference in layer/blob content between two OCI image manifests.
448#[derive(Debug, Serialize)]
449pub struct ManifestDiff<'a> {
450    /// The source container image manifest.
451    #[serde(skip)]
452    pub from: &'a oci_spec::image::ImageManifest,
453    /// The target container image manifest.
454    #[serde(skip)]
455    pub to: &'a oci_spec::image::ImageManifest,
456    /// Layers which are present in the old image but not the new image.
457    #[serde(skip)]
458    pub removed: Vec<&'a oci_spec::image::Descriptor>,
459    /// Layers which are present in the new image but not the old image.
460    #[serde(skip)]
461    pub added: Vec<&'a oci_spec::image::Descriptor>,
462    /// Total number of layers
463    pub total: u64,
464    /// Size of total number of layers.
465    pub total_size: u64,
466    /// Number of layers removed
467    pub n_removed: u64,
468    /// Size of the number of layers removed
469    pub removed_size: u64,
470    /// Number of packages added
471    pub n_added: u64,
472    /// Size of the number of layers added
473    pub added_size: u64,
474}
475
476impl<'a> ManifestDiff<'a> {
477    /// Compute the layer difference between two OCI image manifests.
478    pub fn new(
479        src: &'a oci_spec::image::ImageManifest,
480        dest: &'a oci_spec::image::ImageManifest,
481    ) -> Self {
482        let src_layers = src
483            .layers()
484            .iter()
485            .map(|l| (l.digest().digest(), l))
486            .collect::<HashMap<_, _>>();
487        let dest_layers = dest
488            .layers()
489            .iter()
490            .map(|l| (l.digest().digest(), l))
491            .collect::<HashMap<_, _>>();
492        let mut removed = Vec::new();
493        let mut added = Vec::new();
494        for (blobid, &descriptor) in src_layers.iter() {
495            if !dest_layers.contains_key(blobid) {
496                removed.push(descriptor);
497            }
498        }
499        removed.sort_by(|a, b| a.digest().digest().cmp(b.digest().digest()));
500        for (blobid, &descriptor) in dest_layers.iter() {
501            if !src_layers.contains_key(blobid) {
502                added.push(descriptor);
503            }
504        }
505        added.sort_by(|a, b| a.digest().digest().cmp(b.digest().digest()));
506
507        fn layersum<'a, I: Iterator<Item = &'a oci_spec::image::Descriptor>>(layers: I) -> u64 {
508            layers.map(|layer| layer.size()).sum()
509        }
510        let total = dest_layers.len() as u64;
511        let total_size = layersum(dest.layers().iter());
512        let n_removed = removed.len() as u64;
513        let n_added = added.len() as u64;
514        let removed_size = layersum(removed.iter().copied());
515        let added_size = layersum(added.iter().copied());
516        ManifestDiff {
517            from: src,
518            to: dest,
519            removed,
520            added,
521            total,
522            total_size,
523            n_removed,
524            removed_size,
525            n_added,
526            added_size,
527        }
528    }
529}
530
531impl ManifestDiff<'_> {
532    /// Prints the total, removed and added content between two OCI images
533    pub fn print(&self) {
534        let print_total = self.total;
535        let print_total_size = glib::format_size(self.total_size);
536        let print_n_removed = self.n_removed;
537        let print_removed_size = glib::format_size(self.removed_size);
538        let print_n_added = self.n_added;
539        let print_added_size = glib::format_size(self.added_size);
540        println!("Total new layers: {print_total:<4}  Size: {print_total_size}");
541        println!("Removed layers:   {print_n_removed:<4}  Size: {print_removed_size}");
542        println!("Added layers:     {print_n_added:<4}  Size: {print_added_size}");
543    }
544}
545
546/// Apply default configuration for container image pulls to an existing configuration.
547/// For example, if `authfile` is not set, and `auth_anonymous` is `false`, and a global configuration file exists, it will be used.
548///
549/// If there is no configured explicit subprocess for skopeo, and the process is running
550/// as root, then a default isolation of running the process via `nobody` will be applied.
551pub fn merge_default_container_proxy_opts(
552    config: &mut containers_image_proxy::ImageProxyConfig,
553) -> Result<()> {
554    let user = rustix::process::getuid()
555        .is_root()
556        .then_some(isolation::DEFAULT_UNPRIVILEGED_USER);
557    merge_default_container_proxy_opts_with_isolation(config, user)
558}
559
560/// Apply default configuration for container image pulls, with optional support
561/// for isolation as an unprivileged user.
562pub fn merge_default_container_proxy_opts_with_isolation(
563    config: &mut containers_image_proxy::ImageProxyConfig,
564    isolation_user: Option<&str>,
565) -> Result<()> {
566    let auth_specified =
567        config.auth_anonymous || config.authfile.is_some() || config.auth_data.is_some();
568    if !auth_specified {
569        let root = &Dir::open_ambient_dir("/", cap_std::ambient_authority())?;
570        config.auth_data = crate::globals::get_global_authfile(root)?.map(|a| a.1);
571        // If there's no auth data, then force on anonymous pulls to ensure
572        // that the container stack doesn't try to find it in the standard
573        // container paths.
574        if config.auth_data.is_none() {
575            config.auth_anonymous = true;
576        }
577    }
578    // By default, drop privileges, unless the higher level code
579    // has configured the skopeo command explicitly.
580    let isolation_user = config
581        .skopeo_cmd
582        .is_none()
583        .then_some(isolation_user.as_ref())
584        .flatten();
585    if let Some(user) = isolation_user {
586        // Read the default authfile if it exists and pass it via file descriptor
587        // which will ensure it's readable when we drop privileges.
588        if let Some(authfile) = config.authfile.take() {
589            config.auth_data = Some(std::fs::File::open(authfile)?);
590        }
591        let cmd = crate::isolation::unprivileged_subprocess("skopeo", user);
592        config.skopeo_cmd = Some(cmd);
593    }
594    Ok(())
595}
596
597/// Convenience helper to return the labels, if present.
598pub(crate) fn labels_of(
599    config: &oci_spec::image::ImageConfiguration,
600) -> Option<&HashMap<String, String>> {
601    config.config().as_ref().and_then(|c| c.labels().as_ref())
602}
603
604/// Retrieve the version number from an image configuration.
605pub fn version_for_config(config: &oci_spec::image::ImageConfiguration) -> Option<&str> {
606    if let Some(labels) = labels_of(config) {
607        for k in [oci_spec::image::ANNOTATION_VERSION, LABEL_VERSION] {
608            if let Some(v) = labels.get(k) {
609                return Some(v.as_str());
610            }
611        }
612    }
613    None
614}
615
616pub mod deploy;
617mod encapsulate;
618pub use encapsulate::*;
619mod unencapsulate;
620pub use unencapsulate::*;
621pub mod skopeo;
622pub mod store;
623mod update_detachedmeta;
624pub use update_detachedmeta::*;
625
626use crate::isolation;
627
628#[cfg(test)]
629mod tests {
630    use std::process::Command;
631
632    use containers_image_proxy::ImageProxyConfig;
633
634    use super::*;
635
636    #[test]
637    fn test_serializable_transport() {
638        for v in [
639            Transport::Registry,
640            Transport::ContainerStorage,
641            Transport::OciArchive,
642            Transport::DockerArchive,
643            Transport::OciDir,
644        ] {
645            assert_eq!(Transport::try_from(v.serializable_name()).unwrap(), v);
646        }
647    }
648
649    const INVALID_IRS: &[&str] = &["", "foo://", "docker:blah", "registry:", "foo:bar"];
650    const VALID_IRS: &[&str] = &[
651        "containers-storage:localhost/someimage",
652        "docker://quay.io/exampleos/blah:sometag",
653    ];
654
655    #[test]
656    fn test_imagereference() {
657        let ir: ImageReference = "registry:quay.io/exampleos/blah".try_into().unwrap();
658        assert_eq!(ir.transport, Transport::Registry);
659        assert_eq!(ir.name, "quay.io/exampleos/blah");
660        assert_eq!(ir.to_string(), "docker://quay.io/exampleos/blah");
661
662        for &v in VALID_IRS {
663            ImageReference::try_from(v).unwrap();
664        }
665
666        for &v in INVALID_IRS {
667            if ImageReference::try_from(v).is_ok() {
668                panic!("Should fail to parse: {v}")
669            }
670        }
671        struct Case {
672            s: &'static str,
673            transport: Transport,
674            name: &'static str,
675        }
676        for case in [
677            Case {
678                s: "oci:somedir",
679                transport: Transport::OciDir,
680                name: "somedir",
681            },
682            Case {
683                s: "dir:/some/dir/blah",
684                transport: Transport::Dir,
685                name: "/some/dir/blah",
686            },
687            Case {
688                s: "oci-archive:/path/to/foo.ociarchive",
689                transport: Transport::OciArchive,
690                name: "/path/to/foo.ociarchive",
691            },
692            Case {
693                s: "docker-archive:/path/to/foo.dockerarchive",
694                transport: Transport::DockerArchive,
695                name: "/path/to/foo.dockerarchive",
696            },
697            Case {
698                s: "containers-storage:localhost/someimage:blah",
699                transport: Transport::ContainerStorage,
700                name: "localhost/someimage:blah",
701            },
702        ] {
703            let ir: ImageReference = case.s.try_into().unwrap();
704            assert_eq!(ir.transport, case.transport);
705            assert_eq!(ir.name, case.name);
706            let reserialized = ir.to_string();
707            assert_eq!(case.s, reserialized.as_str());
708        }
709    }
710
711    #[test]
712    fn test_ostreeimagereference() {
713        // Test both long form `ostree-remote-image:$myremote:registry` and the
714        // shorthand `ostree-remote-registry:$myremote`.
715        let ir_s = "ostree-remote-image:myremote:registry:quay.io/exampleos/blah";
716        let ir_registry = "ostree-remote-registry:myremote:quay.io/exampleos/blah";
717        for &ir_s in &[ir_s, ir_registry] {
718            let ir: OstreeImageReference = ir_s.try_into().unwrap();
719            assert_eq!(
720                ir.sigverify,
721                SignatureSource::OstreeRemote("myremote".to_string())
722            );
723            assert_eq!(ir.imgref.transport, Transport::Registry);
724            assert_eq!(ir.imgref.name, "quay.io/exampleos/blah");
725            assert_eq!(
726                ir.to_string(),
727                "ostree-remote-image:myremote:docker://quay.io/exampleos/blah"
728            );
729        }
730
731        // Also verify our FromStr impls
732
733        let ir: OstreeImageReference = ir_s.try_into().unwrap();
734        assert_eq!(ir, OstreeImageReference::from_str(ir_s).unwrap());
735        // test our Eq implementation
736        assert_eq!(&ir, &OstreeImageReference::try_from(ir_registry).unwrap());
737
738        let ir_s = "ostree-image-signed:docker://quay.io/exampleos/blah";
739        let ir: OstreeImageReference = ir_s.try_into().unwrap();
740        assert_eq!(ir.sigverify, SignatureSource::ContainerPolicy);
741        assert_eq!(ir.imgref.transport, Transport::Registry);
742        assert_eq!(ir.imgref.name, "quay.io/exampleos/blah");
743        assert_eq!(ir.to_string(), ir_s);
744        assert_eq!(format!("{:#}", &ir), ir_s);
745
746        let ir_s = "ostree-unverified-image:docker://quay.io/exampleos/blah";
747        let ir: OstreeImageReference = ir_s.try_into().unwrap();
748        assert_eq!(ir.sigverify, SignatureSource::ContainerPolicyAllowInsecure);
749        assert_eq!(ir.imgref.transport, Transport::Registry);
750        assert_eq!(ir.imgref.name, "quay.io/exampleos/blah");
751        assert_eq!(
752            ir.to_string(),
753            "ostree-unverified-registry:quay.io/exampleos/blah"
754        );
755        let ir_shorthand =
756            OstreeImageReference::try_from("ostree-unverified-registry:quay.io/exampleos/blah")
757                .unwrap();
758        assert_eq!(&ir_shorthand, &ir);
759        assert_eq!(format!("{:#}", &ir), "docker://quay.io/exampleos/blah");
760    }
761
762    #[test]
763    fn test_merge_authopts() {
764        // Verify idempotence of authentication processing
765        let mut c = ImageProxyConfig::default();
766        let authf = std::fs::File::open("/dev/null").unwrap();
767        c.auth_data = Some(authf);
768        super::merge_default_container_proxy_opts_with_isolation(&mut c, None).unwrap();
769        assert!(!c.auth_anonymous);
770        assert!(c.authfile.is_none());
771        assert!(c.auth_data.is_some());
772        assert!(c.skopeo_cmd.is_none());
773        super::merge_default_container_proxy_opts_with_isolation(&mut c, None).unwrap();
774        assert!(!c.auth_anonymous);
775        assert!(c.authfile.is_none());
776        assert!(c.auth_data.is_some());
777        assert!(c.skopeo_cmd.is_none());
778
779        // Verify interaction with explicit isolation
780        let mut c = ImageProxyConfig {
781            skopeo_cmd: Some(Command::new("skopeo")),
782            ..Default::default()
783        };
784        super::merge_default_container_proxy_opts_with_isolation(&mut c, Some("foo")).unwrap();
785        assert_eq!(c.skopeo_cmd.unwrap().get_program(), "skopeo");
786    }
787}