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}