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.
431pub fn merge_default_container_proxy_opts(
432 config: &mut containers_image_proxy::ImageProxyConfig,
433) -> Result<()> {
434 let auth_specified =
435 config.auth_anonymous || config.authfile.is_some() || config.auth_data.is_some();
436 if !auth_specified {
437 let root = &Dir::open_ambient_dir("/", cap_std::ambient_authority())?;
438 config.auth_data = crate::globals::get_global_authfile(root)?.map(|a| a.1);
439 // If there's no auth data, then force on anonymous pulls to ensure
440 // that the container stack doesn't try to find it in the standard
441 // container paths.
442 if config.auth_data.is_none() {
443 config.auth_anonymous = true;
444 }
445 }
446 Ok(())
447}
448
449/// Convenience helper to return the labels, if present.
450pub(crate) fn labels_of(
451 config: &oci_spec::image::ImageConfiguration,
452) -> Option<&HashMap<String, String>> {
453 config.config().as_ref().and_then(|c| c.labels().as_ref())
454}
455
456/// Retrieve the version number from an image configuration.
457pub fn version_for_config(config: &oci_spec::image::ImageConfiguration) -> Option<&str> {
458 if let Some(labels) = labels_of(config) {
459 for k in [oci_spec::image::ANNOTATION_VERSION, LABEL_VERSION] {
460 if let Some(v) = labels.get(k) {
461 return Some(v.as_str());
462 }
463 }
464 }
465 None
466}
467
468pub mod deploy;
469mod encapsulate;
470pub use encapsulate::*;
471mod unencapsulate;
472pub use unencapsulate::*;
473pub mod skopeo;
474pub mod store;
475mod update_detachedmeta;
476pub use update_detachedmeta::*;
477
478#[cfg(test)]
479mod tests {
480 use std::process::Command;
481
482 use containers_image_proxy::ImageProxyConfig;
483
484 use super::*;
485
486 #[test]
487 fn test_serializable_transport() {
488 for v in [
489 Transport::Registry,
490 Transport::ContainerStorage,
491 Transport::OciArchive,
492 Transport::DockerArchive,
493 Transport::OciDir,
494 ] {
495 assert_eq!(Transport::try_from(v.to_string().as_ref()).unwrap(), v);
496 }
497 }
498
499 const INVALID_IRS: &[&str] = &["", "foo://", "docker:blah", "registry:", "foo:bar"];
500 const VALID_IRS: &[&str] = &[
501 "containers-storage:localhost/someimage",
502 "docker://quay.io/exampleos/blah:sometag",
503 ];
504
505 #[test]
506 fn test_imagereference() {
507 let ir: ImageReference = "registry:quay.io/exampleos/blah".try_into().unwrap();
508 assert_eq!(ir.transport, Transport::Registry);
509 assert_eq!(ir.name, "quay.io/exampleos/blah");
510 assert_eq!(ir.to_string(), "docker://quay.io/exampleos/blah");
511
512 for &v in VALID_IRS {
513 ImageReference::try_from(v).unwrap();
514 }
515
516 for &v in INVALID_IRS {
517 if ImageReference::try_from(v).is_ok() {
518 panic!("Should fail to parse: {v}")
519 }
520 }
521 struct Case {
522 s: &'static str,
523 transport: Transport,
524 name: &'static str,
525 }
526 for case in [
527 Case {
528 s: "oci:somedir",
529 transport: Transport::OciDir,
530 name: "somedir",
531 },
532 Case {
533 s: "dir:/some/dir/blah",
534 transport: Transport::Dir,
535 name: "/some/dir/blah",
536 },
537 Case {
538 s: "oci-archive:/path/to/foo.ociarchive",
539 transport: Transport::OciArchive,
540 name: "/path/to/foo.ociarchive",
541 },
542 Case {
543 s: "docker-archive:/path/to/foo.dockerarchive",
544 transport: Transport::DockerArchive,
545 name: "/path/to/foo.dockerarchive",
546 },
547 Case {
548 s: "containers-storage:localhost/someimage:blah",
549 transport: Transport::ContainerStorage,
550 name: "localhost/someimage:blah",
551 },
552 ] {
553 let ir: ImageReference = case.s.try_into().unwrap();
554 assert_eq!(ir.transport, case.transport);
555 assert_eq!(ir.name, case.name);
556 let reserialized = ir.to_string();
557 assert_eq!(case.s, reserialized.as_str());
558 }
559 }
560
561 #[test]
562 fn test_ostreeimagereference() {
563 // Test both long form `ostree-remote-image:$myremote:registry` and the
564 // shorthand `ostree-remote-registry:$myremote`.
565 let ir_s = "ostree-remote-image:myremote:registry:quay.io/exampleos/blah";
566 let ir_registry = "ostree-remote-registry:myremote:quay.io/exampleos/blah";
567 for &ir_s in &[ir_s, ir_registry] {
568 let ir: OstreeImageReference = ir_s.try_into().unwrap();
569 assert_eq!(
570 ir.sigverify,
571 SignatureSource::OstreeRemote("myremote".to_string())
572 );
573 assert_eq!(ir.imgref.transport, Transport::Registry);
574 assert_eq!(ir.imgref.name, "quay.io/exampleos/blah");
575 assert_eq!(
576 ir.to_string(),
577 "ostree-remote-image:myremote:docker://quay.io/exampleos/blah"
578 );
579 }
580
581 // Also verify our FromStr impls
582
583 let ir: OstreeImageReference = ir_s.try_into().unwrap();
584 assert_eq!(ir, OstreeImageReference::from_str(ir_s).unwrap());
585 // test our Eq implementation
586 assert_eq!(&ir, &OstreeImageReference::try_from(ir_registry).unwrap());
587
588 let ir_s = "ostree-image-signed:docker://quay.io/exampleos/blah";
589 let ir: OstreeImageReference = ir_s.try_into().unwrap();
590 assert_eq!(ir.sigverify, SignatureSource::ContainerPolicy);
591 assert_eq!(ir.imgref.transport, Transport::Registry);
592 assert_eq!(ir.imgref.name, "quay.io/exampleos/blah");
593 assert_eq!(ir.to_string(), ir_s);
594 assert_eq!(format!("{:#}", &ir), ir_s);
595
596 let ir_s = "ostree-unverified-image:docker://quay.io/exampleos/blah";
597 let ir: OstreeImageReference = ir_s.try_into().unwrap();
598 assert_eq!(ir.sigverify, SignatureSource::ContainerPolicyAllowInsecure);
599 assert_eq!(ir.imgref.transport, Transport::Registry);
600 assert_eq!(ir.imgref.name, "quay.io/exampleos/blah");
601 assert_eq!(
602 ir.to_string(),
603 "ostree-unverified-registry:quay.io/exampleos/blah"
604 );
605 let ir_shorthand =
606 OstreeImageReference::try_from("ostree-unverified-registry:quay.io/exampleos/blah")
607 .unwrap();
608 assert_eq!(&ir_shorthand, &ir);
609 assert_eq!(format!("{:#}", &ir), "docker://quay.io/exampleos/blah");
610 }
611
612 #[test]
613 fn test_merge_authopts() {
614 // Verify idempotence of authentication processing
615 let mut c = ImageProxyConfig::default();
616 let authf = std::fs::File::open("/dev/null").unwrap();
617 c.auth_data = Some(authf);
618 super::merge_default_container_proxy_opts(&mut c).unwrap();
619 assert!(!c.auth_anonymous);
620 assert!(c.authfile.is_none());
621 assert!(c.auth_data.is_some());
622 assert!(c.skopeo_cmd.is_none());
623 super::merge_default_container_proxy_opts(&mut c).unwrap();
624 assert!(!c.auth_anonymous);
625 assert!(c.authfile.is_none());
626 assert!(c.auth_data.is_some());
627 assert!(c.skopeo_cmd.is_none());
628
629 // Verify interaction with explicit skopeo command
630 let mut c = ImageProxyConfig::default();
631 c.skopeo_cmd = Some(Command::new("skopeo"));
632 super::merge_default_container_proxy_opts(&mut c).unwrap();
633 assert_eq!(c.skopeo_cmd.unwrap().get_program(), "skopeo");
634 }
635}