ostree_ext/
cli.rs

1//! # Commandline parsing
2//!
3//! While there is a separate `ostree-ext-cli` crate that
4//! can be installed and used directly, the CLI code is
5//! also exported as a library too, so that projects
6//! such as `rpm-ostree` can directly reuse it.
7
8use anyhow::{Context, Result};
9use camino::{Utf8Path, Utf8PathBuf};
10use canon_json::CanonJsonSerialize;
11use cap_std::fs::Dir;
12use cap_std_ext::cap_std;
13use cap_std_ext::prelude::CapStdExtDirExt;
14use clap::{Parser, Subcommand};
15use fn_error_context::context;
16use indexmap::IndexMap;
17use io_lifetimes::AsFd;
18use ostree::{gio, glib};
19use std::borrow::Cow;
20use std::collections::BTreeMap;
21use std::ffi::OsString;
22use std::fs::File;
23use std::io::{BufReader, BufWriter, Write};
24use std::num::NonZeroU32;
25use std::path::PathBuf;
26use std::process::Command;
27use tokio::sync::mpsc::Receiver;
28
29use crate::chunking::{ObjectMetaSized, ObjectSourceMetaSized};
30use crate::commit::container_commit;
31use crate::container::store::{ExportToOCIOpts, ImportProgress, LayerProgress, PreparedImport};
32use crate::container::{self as ostree_container, ManifestDiff};
33use crate::container::{Config, ImageReference, OstreeImageReference};
34use crate::objectsource::ObjectSourceMeta;
35use crate::sysroot::SysrootLock;
36use ostree_container::store::{ImageImporter, PrepareResult};
37use serde::{Deserialize, Serialize};
38
39/// Parse an [`OstreeImageReference`] from a CLI argument.
40pub fn parse_imgref(s: &str) -> Result<OstreeImageReference> {
41    OstreeImageReference::try_from(s)
42}
43
44/// Parse a base [`ImageReference`] from a CLI argument.
45pub fn parse_base_imgref(s: &str) -> Result<ImageReference> {
46    ImageReference::try_from(s)
47}
48
49/// Parse an [`ostree::Repo`] from a CLI argument.
50pub fn parse_repo(s: &Utf8Path) -> Result<ostree::Repo> {
51    let repofd = cap_std::fs::Dir::open_ambient_dir(s, cap_std::ambient_authority())
52        .with_context(|| format!("Opening directory at '{s}'"))?;
53    ostree::Repo::open_at_dir(repofd.as_fd(), ".")
54        .with_context(|| format!("Opening ostree repository at '{s}'"))
55}
56
57/// Options for importing a tar archive.
58#[derive(Debug, Parser)]
59pub(crate) struct ImportOpts {
60    /// Path to the repository
61    #[clap(long, value_parser)]
62    repo: Utf8PathBuf,
63
64    /// Path to a tar archive; if unspecified, will be stdin.  Currently the tar archive must not be compressed.
65    path: Option<String>,
66}
67
68/// Options for exporting a tar archive.
69#[derive(Debug, Parser)]
70pub(crate) struct ExportOpts {
71    /// Path to the repository
72    #[clap(long, value_parser)]
73    repo: Utf8PathBuf,
74
75    /// The format version.  Must be 1.
76    #[clap(long, hide(true))]
77    format_version: u32,
78
79    /// The ostree ref or commit to export
80    rev: String,
81}
82
83/// Options for import/export to tar archives.
84#[derive(Debug, Subcommand)]
85pub(crate) enum TarOpts {
86    /// Import a tar archive (currently, must not be compressed)
87    Import(ImportOpts),
88
89    /// Write a tar archive to stdout
90    Export(ExportOpts),
91}
92
93/// Options for container import/export.
94#[derive(Debug, Subcommand)]
95pub(crate) enum ContainerOpts {
96    #[clap(alias = "import")]
97    /// Import an ostree commit embedded in a remote container image
98    Unencapsulate {
99        /// Path to the repository
100        #[clap(long, value_parser)]
101        repo: Utf8PathBuf,
102
103        #[clap(flatten)]
104        proxyopts: ContainerProxyOpts,
105
106        /// Image reference, e.g. registry:quay.io/exampleos/exampleos:latest
107        #[clap(value_parser = parse_imgref)]
108        imgref: OstreeImageReference,
109
110        /// Create an ostree ref pointing to the imported commit
111        #[clap(long)]
112        write_ref: Option<String>,
113
114        /// Don't display progress
115        #[clap(long)]
116        quiet: bool,
117    },
118
119    /// Print information about an exported ostree-container image.
120    Info {
121        /// Image reference, e.g. registry:quay.io/exampleos/exampleos:latest
122        #[clap(value_parser = parse_imgref)]
123        imgref: OstreeImageReference,
124    },
125
126    /// Wrap an ostree commit into a container image.
127    ///
128    /// The resulting container image will have a single layer, which is
129    /// very often not what's desired. To handle things more intelligently,
130    /// you will need to use (or create) a higher level tool that splits
131    /// content into distinct "chunks"; functionality for this is
132    /// exposed by the API but not CLI currently.
133    #[clap(alias = "export")]
134    Encapsulate {
135        /// Path to the repository
136        #[clap(long, value_parser)]
137        repo: Utf8PathBuf,
138
139        /// The ostree ref or commit to export
140        rev: String,
141
142        /// Image reference, e.g. registry:quay.io/exampleos/exampleos:latest
143        #[clap(value_parser = parse_base_imgref)]
144        imgref: ImageReference,
145
146        /// Additional labels for the container
147        #[clap(name = "label", long, short)]
148        labels: Vec<String>,
149
150        #[clap(long)]
151        /// Path to Docker-formatted authentication file.
152        authfile: Option<PathBuf>,
153
154        /// Path to a JSON-formatted serialized container configuration; this is the
155        /// `config` property of https://github.com/opencontainers/image-spec/blob/main/config.md
156        #[clap(long)]
157        config: Option<Utf8PathBuf>,
158
159        /// Propagate an OSTree commit metadata key to container label
160        #[clap(name = "copymeta", long)]
161        copy_meta_keys: Vec<String>,
162
163        /// Propagate an optionally-present OSTree commit metadata key to container label
164        #[clap(name = "copymeta-opt", long)]
165        copy_meta_opt_keys: Vec<String>,
166
167        /// Corresponds to the Dockerfile `CMD` instruction.
168        #[clap(long)]
169        cmd: Option<Vec<String>>,
170
171        /// Compress at the fastest level (e.g. gzip level 1)
172        #[clap(long)]
173        compression_fast: bool,
174
175        /// Path to a JSON-formatted content meta object.
176        #[clap(long)]
177        contentmeta: Option<Utf8PathBuf>,
178    },
179
180    /// Perform build-time checking and canonicalization.
181    /// This is presently an optional command, but may become required in the future.
182    Commit,
183
184    /// Commands for working with (possibly layered, non-encapsulated) container images.
185    #[clap(subcommand)]
186    Image(ContainerImageOpts),
187
188    /// Compare the contents of two OCI compliant images.
189    Compare {
190        /// Image reference, e.g. ostree-remote-image:someremote:registry:quay.io/exampleos/exampleos:latest
191        #[clap(value_parser = parse_imgref)]
192        imgref_old: OstreeImageReference,
193
194        /// Image reference, e.g. ostree-remote-image:someremote:registry:quay.io/exampleos/exampleos:latest
195        #[clap(value_parser = parse_imgref)]
196        imgref_new: OstreeImageReference,
197    },
198}
199
200/// Options for container image fetching.
201#[derive(Debug, Parser)]
202pub(crate) struct ContainerProxyOpts {
203    #[clap(long)]
204    /// Do not use default authentication files.
205    auth_anonymous: bool,
206
207    #[clap(long)]
208    /// Path to Docker-formatted authentication file.
209    authfile: Option<PathBuf>,
210
211    #[clap(long)]
212    /// Directory with certificates (*.crt, *.cert, *.key) used to connect to registry
213    /// Equivalent to `skopeo --cert-dir`
214    cert_dir: Option<PathBuf>,
215
216    #[clap(long)]
217    /// Skip TLS verification.
218    insecure_skip_tls_verification: bool,
219}
220
221/// Options for import/export to tar archives.
222#[derive(Debug, Subcommand)]
223pub(crate) enum ContainerImageOpts {
224    /// List container images
225    List {
226        /// Path to the repository
227        #[clap(long, value_parser)]
228        repo: Utf8PathBuf,
229    },
230
231    /// Pull (or update) a container image.
232    Pull {
233        /// Path to the repository
234        #[clap(value_parser)]
235        repo: Utf8PathBuf,
236
237        /// Image reference, e.g. ostree-remote-image:someremote:registry:quay.io/exampleos/exampleos:latest
238        #[clap(value_parser = parse_imgref)]
239        imgref: OstreeImageReference,
240
241        /// File to which to write the resulting OSTree commit digest
242        #[clap(long)]
243        ostree_digestfile: Option<Utf8PathBuf>,
244
245        #[clap(flatten)]
246        proxyopts: ContainerProxyOpts,
247
248        /// Don't display progress
249        #[clap(long)]
250        quiet: bool,
251
252        /// Just check for an updated manifest, but do not download associated container layers.
253        /// If an updated manifest is found, a file at the provided path will be created and contain
254        /// the new manifest.
255        #[clap(long)]
256        check: Option<Utf8PathBuf>,
257    },
258
259    /// Output metadata about an already stored container image.
260    History {
261        /// Path to the repository
262        #[clap(long, value_parser)]
263        repo: Utf8PathBuf,
264
265        /// Container image reference, e.g. registry:quay.io/exampleos/exampleos:latest
266        #[clap(value_parser = parse_base_imgref)]
267        imgref: ImageReference,
268    },
269
270    /// Output manifest or configuration for an already stored container image.
271    Metadata {
272        /// Path to the repository
273        #[clap(long, value_parser)]
274        repo: Utf8PathBuf,
275
276        /// Container image reference, e.g. registry:quay.io/exampleos/exampleos:latest
277        #[clap(value_parser = parse_base_imgref)]
278        imgref: ImageReference,
279
280        /// Output the config, not the manifest
281        #[clap(long)]
282        config: bool,
283    },
284
285    /// Remove metadata for a cached update.
286    ClearCachedUpdate {
287        /// Path to the repository
288        #[clap(long, value_parser)]
289        repo: Utf8PathBuf,
290
291        /// Container image reference, e.g. registry:quay.io/exampleos/exampleos:latest
292        #[clap(value_parser = parse_base_imgref)]
293        imgref: ImageReference,
294    },
295
296    /// Copy a pulled container image from one repo to another.
297    Copy {
298        /// Path to the source repository
299        #[clap(long, value_parser)]
300        src_repo: Utf8PathBuf,
301
302        /// Path to the destination repository
303        #[clap(long, value_parser)]
304        dest_repo: Utf8PathBuf,
305
306        /// Image reference, e.g. ostree-remote-image:someremote:registry:quay.io/exampleos/exampleos:latest
307        #[clap(value_parser = parse_imgref)]
308        imgref: OstreeImageReference,
309    },
310
311    /// Re-export a fetched image.
312    ///
313    /// Unlike `encapsulate`, this verb handles layered images, and will
314    /// also automatically preserve chunked structure from the fetched image.
315    Reexport {
316        /// Path to the repository
317        #[clap(long, value_parser)]
318        repo: Utf8PathBuf,
319
320        /// Source image reference, e.g. registry:quay.io/exampleos/exampleos:latest
321        #[clap(value_parser = parse_base_imgref)]
322        src_imgref: ImageReference,
323
324        /// Destination image reference, e.g. registry:quay.io/exampleos/exampleos:latest
325        #[clap(value_parser = parse_base_imgref)]
326        dest_imgref: ImageReference,
327
328        #[clap(long)]
329        /// Path to Docker-formatted authentication file.
330        authfile: Option<PathBuf>,
331
332        /// Compress at the fastest level (e.g. gzip level 1)
333        #[clap(long)]
334        compression_fast: bool,
335    },
336
337    /// Replace the detached metadata (e.g. to add a signature)
338    ReplaceDetachedMetadata {
339        /// Path to the source repository
340        #[clap(long)]
341        #[clap(value_parser = parse_base_imgref)]
342        src: ImageReference,
343
344        /// Target image
345        #[clap(long)]
346        #[clap(value_parser = parse_base_imgref)]
347        dest: ImageReference,
348
349        /// Path to file containing new detached metadata; if not provided,
350        /// any existing detached metadata will be deleted.
351        contents: Option<Utf8PathBuf>,
352    },
353
354    /// Unreference one or more pulled container images and perform a garbage collection.
355    Remove {
356        /// Path to the repository
357        #[clap(long, value_parser)]
358        repo: Utf8PathBuf,
359
360        /// Image reference, e.g. quay.io/exampleos/exampleos:latest
361        #[clap(value_parser = parse_base_imgref)]
362        imgrefs: Vec<ImageReference>,
363
364        /// Do not garbage collect unused layers
365        #[clap(long)]
366        skip_gc: bool,
367    },
368
369    /// Garbage collect unreferenced image layer references.
370    PruneLayers {
371        /// Path to the repository
372        #[clap(long, value_parser)]
373        repo: Utf8PathBuf,
374    },
375
376    /// Garbage collect unreferenced image layer references.
377    PruneImages {
378        /// Path to the system root
379        #[clap(long)]
380        sysroot: Utf8PathBuf,
381
382        #[clap(long)]
383        /// Also prune layers
384        and_layers: bool,
385
386        #[clap(long, conflicts_with = "and_layers")]
387        /// Also prune layers and OSTree objects
388        full: bool,
389    },
390
391    /// Perform initial deployment for a container image
392    Deploy {
393        /// Path to the system root
394        #[clap(long)]
395        sysroot: Option<String>,
396
397        /// Name for the state directory, also known as "osname".
398        /// If the current system is booted via ostree, then this will default to the booted stateroot.
399        /// Otherwise, the default is `default`.
400        #[clap(long)]
401        stateroot: Option<String>,
402
403        /// Source image reference, e.g. ostree-remote-image:someremote:registry:quay.io/exampleos/exampleos@sha256:abcd...
404        /// This conflicts with `--image`.
405        /// This conflicts with `--image`. Supports `registry:`, `docker://`, `oci:`, `oci-archive:`, `containers-storage:`, and `dir:`
406        #[clap(long, required_unless_present = "image")]
407        imgref: Option<String>,
408
409        /// Name of the container image; for the `registry` transport this would be e.g. `quay.io/exampleos/foo:latest`.
410        /// This conflicts with `--imgref`.
411        #[clap(long, required_unless_present = "imgref")]
412        image: Option<String>,
413
414        /// The transport; e.g. registry, oci, oci-archive.  The default is `registry`.
415        #[clap(long)]
416        transport: Option<String>,
417
418        /// This option does nothing and is now deprecated.  Signature verification enforcement
419        /// proved to not be viable.
420        ///
421        /// If you want to still enforce it, use `--enforce-container-sigpolicy`.
422        #[clap(long, conflicts_with = "enforce_container_sigpolicy")]
423        no_signature_verification: bool,
424
425        /// Require that the containers-storage stack
426        #[clap(long)]
427        enforce_container_sigpolicy: bool,
428
429        /// Enable verification via an ostree remote
430        #[clap(long)]
431        ostree_remote: Option<String>,
432
433        #[clap(flatten)]
434        proxyopts: ContainerProxyOpts,
435
436        /// Target image reference, e.g. ostree-remote-image:someremote:registry:quay.io/exampleos/exampleos:latest
437        ///
438        /// If specified, `--imgref` will be used as a source, but this reference will be emitted into the origin
439        /// so that later OS updates pull from it.
440        #[clap(long)]
441        #[clap(value_parser = parse_imgref)]
442        target_imgref: Option<OstreeImageReference>,
443
444        /// If set, only write the layer refs, but not the final container image reference.  This
445        /// allows generating a disk image that when booted uses "native ostree", but has layer
446        /// references "pre-cached" such that a container image fetch will avoid redownloading
447        /// everything.
448        #[clap(long)]
449        no_imgref: bool,
450
451        #[clap(long)]
452        /// Add a kernel argument
453        karg: Option<Vec<String>>,
454
455        /// Write the deployed checksum to this file
456        #[clap(long)]
457        write_commitid_to: Option<Utf8PathBuf>,
458    },
459}
460
461/// Options for deployment repair.
462#[derive(Debug, Parser)]
463pub(crate) enum ProvisionalRepairOpts {
464    AnalyzeInodes {
465        /// Path to the repository
466        #[clap(long, value_parser)]
467        repo: Utf8PathBuf,
468
469        /// Print additional information
470        #[clap(long)]
471        verbose: bool,
472
473        /// Serialize the repair result to this file as JSON
474        #[clap(long)]
475        write_result_to: Option<Utf8PathBuf>,
476    },
477
478    Repair {
479        /// Path to the sysroot
480        #[clap(long, value_parser)]
481        sysroot: Utf8PathBuf,
482
483        /// Do not mutate any system state
484        #[clap(long)]
485        dry_run: bool,
486
487        /// Serialize the repair result to this file as JSON
488        #[clap(long)]
489        write_result_to: Option<Utf8PathBuf>,
490
491        /// Print additional information
492        #[clap(long)]
493        verbose: bool,
494    },
495}
496
497/// Options for the Integrity Measurement Architecture (IMA).
498#[derive(Debug, Parser)]
499pub(crate) struct ImaSignOpts {
500    /// Path to the repository
501    #[clap(long, value_parser)]
502    repo: Utf8PathBuf,
503
504    /// The ostree ref or commit to use as a base
505    src_rev: String,
506    /// The ostree ref to use for writing the signed commit
507    target_ref: String,
508
509    /// Digest algorithm
510    algorithm: String,
511    /// Path to IMA key
512    key: Utf8PathBuf,
513
514    #[clap(long)]
515    /// Overwrite any existing signatures
516    overwrite: bool,
517}
518
519/// Options for internal testing
520#[derive(Debug, Subcommand)]
521pub(crate) enum TestingOpts {
522    /// Detect the current environment
523    DetectEnv,
524    /// Generate a test fixture
525    CreateFixture,
526    /// Execute integration tests, assuming mutable environment
527    Run,
528    /// Execute IMA tests
529    RunIMA,
530    FilterTar,
531}
532
533/// Options for man page generation
534#[derive(Debug, Parser)]
535pub(crate) struct ManOpts {
536    #[clap(long)]
537    /// Output to this directory
538    directory: Utf8PathBuf,
539}
540
541/// Toplevel options for extended ostree functionality.
542#[derive(Debug, Parser)]
543#[clap(name = "ostree-ext")]
544#[clap(rename_all = "kebab-case")]
545#[allow(clippy::large_enum_variant)]
546pub(crate) enum Opt {
547    /// Import and export to tar
548    #[clap(subcommand)]
549    Tar(TarOpts),
550    /// Import and export to a container image
551    #[clap(subcommand)]
552    Container(ContainerOpts),
553    /// IMA signatures
554    ImaSign(ImaSignOpts),
555    /// Internal integration testing helpers.
556    #[clap(hide(true), subcommand)]
557    #[cfg(feature = "internal-testing-api")]
558    InternalOnlyForTesting(TestingOpts),
559    #[clap(hide(true))]
560    #[cfg(feature = "docgen")]
561    Man(ManOpts),
562    #[clap(hide = true, subcommand)]
563    ProvisionalRepair(ProvisionalRepairOpts),
564}
565
566#[allow(clippy::from_over_into)]
567impl Into<ostree_container::store::ImageProxyConfig> for ContainerProxyOpts {
568    fn into(self) -> ostree_container::store::ImageProxyConfig {
569        ostree_container::store::ImageProxyConfig {
570            auth_anonymous: self.auth_anonymous,
571            authfile: self.authfile,
572            certificate_directory: self.cert_dir,
573            insecure_skip_tls_verification: Some(self.insecure_skip_tls_verification),
574            ..Default::default()
575        }
576    }
577}
578
579/// Import a tar archive containing an ostree commit.
580async fn tar_import(opts: &ImportOpts) -> Result<()> {
581    let repo = parse_repo(&opts.repo)?;
582    let imported = if let Some(path) = opts.path.as_ref() {
583        let instream = tokio::fs::File::open(path).await?;
584        crate::tar::import_tar(&repo, instream, None).await?
585    } else {
586        let stdin = tokio::io::stdin();
587        crate::tar::import_tar(&repo, stdin, None).await?
588    };
589    println!("Imported: {imported}");
590    Ok(())
591}
592
593/// Export a tar archive containing an ostree commit.
594fn tar_export(opts: &ExportOpts) -> Result<()> {
595    let repo = parse_repo(&opts.repo)?;
596    #[allow(clippy::needless_update)]
597    let subopts = crate::tar::ExportOptions {
598        ..Default::default()
599    };
600    crate::tar::export_commit(&repo, opts.rev.as_str(), std::io::stdout(), Some(subopts))?;
601    Ok(())
602}
603
604/// Render an import progress notification as a string.
605pub fn layer_progress_format(p: &ImportProgress) -> String {
606    let (starting, s, layer) = match p {
607        ImportProgress::OstreeChunkStarted(v) => (true, "ostree chunk", v),
608        ImportProgress::OstreeChunkCompleted(v) => (false, "ostree chunk", v),
609        ImportProgress::DerivedLayerStarted(v) => (true, "layer", v),
610        ImportProgress::DerivedLayerCompleted(v) => (false, "layer", v),
611    };
612    // podman outputs 12 characters of digest, let's add 7 for `sha256:`.
613    let short_digest = layer
614        .digest()
615        .digest()
616        .chars()
617        .take(12 + 7)
618        .collect::<String>();
619    if starting {
620        let size = glib::format_size(layer.size());
621        format!("Fetching {s} {short_digest} ({size})")
622    } else {
623        format!("Fetched {s} {short_digest}")
624    }
625}
626
627/// Write container fetch progress to standard output.
628pub async fn handle_layer_progress_print(
629    mut layers: Receiver<ImportProgress>,
630    mut layer_bytes: tokio::sync::watch::Receiver<Option<LayerProgress>>,
631) {
632    let style = indicatif::ProgressStyle::default_bar();
633    let pb = indicatif::ProgressBar::new(100);
634    pb.set_style(
635        style
636            .template("{prefix} {bytes} [{bar:20}] ({eta}) {msg}")
637            .unwrap(),
638    );
639    loop {
640        tokio::select! {
641            // Always handle layer changes first.
642            biased;
643            layer = layers.recv() => {
644                if let Some(l) = layer {
645                    if l.is_starting() {
646                        pb.set_position(0);
647                    } else {
648                        pb.finish();
649                    }
650                    pb.set_message(layer_progress_format(&l));
651                } else {
652                    // If the receiver is disconnected, then we're done
653                    break
654                };
655            },
656            r = layer_bytes.changed() => {
657                if r.is_err() {
658                    // If the receiver is disconnected, then we're done
659                    break
660                }
661                let bytes = layer_bytes.borrow();
662                if let Some(bytes) = &*bytes {
663                    pb.set_length(bytes.total);
664                    pb.set_position(bytes.fetched);
665                }
666            }
667
668        }
669    }
670}
671
672/// Write the status of layers to download.
673pub fn print_layer_status(prep: &PreparedImport) {
674    if let Some(status) = prep.format_layer_status() {
675        println!("{status}");
676        let _ = std::io::stdout().flush();
677    }
678}
679
680/// Write a deprecation notice, and sleep for 3 seconds.
681pub async fn print_deprecated_warning(msg: &str) {
682    eprintln!("warning: {msg}");
683    tokio::time::sleep(std::time::Duration::from_secs(3)).await
684}
685
686/// Import a container image with an encapsulated ostree commit.
687async fn container_import(
688    repo: &ostree::Repo,
689    imgref: &OstreeImageReference,
690    proxyopts: ContainerProxyOpts,
691    write_ref: Option<&str>,
692    quiet: bool,
693) -> Result<()> {
694    let target = indicatif::ProgressDrawTarget::stdout();
695    let style = indicatif::ProgressStyle::default_bar();
696    let pb = (!quiet).then(|| {
697        let pb = indicatif::ProgressBar::new_spinner();
698        pb.set_draw_target(target);
699        pb.set_style(style.template("{spinner} {prefix} {msg}").unwrap());
700        pb.enable_steady_tick(std::time::Duration::from_millis(200));
701        pb.set_message("Downloading...");
702        pb
703    });
704    let importer = ImageImporter::new(repo, imgref, proxyopts.into()).await?;
705    let import = importer.unencapsulate().await;
706    // Ensure we finish the progress bar before potentially propagating an error
707    if let Some(pb) = pb.as_ref() {
708        pb.finish();
709    }
710    let import = import?;
711    if let Some(warning) = import.deprecated_warning.as_deref() {
712        print_deprecated_warning(warning).await;
713    }
714    if let Some(write_ref) = write_ref {
715        repo.set_ref_immediate(
716            None,
717            write_ref,
718            Some(import.ostree_commit.as_str()),
719            gio::Cancellable::NONE,
720        )?;
721        println!(
722            "Imported: {} => {}",
723            write_ref,
724            import.ostree_commit.as_str()
725        );
726    } else {
727        println!("Imported: {}", import.ostree_commit);
728    }
729
730    Ok(())
731}
732
733/// Grouping of metadata about an object.
734#[derive(Debug, Default, Serialize, Deserialize)]
735pub struct RawMeta {
736    /// The metadata format version. Should be set to 1.
737    pub version: u32,
738    /// The image creation timestamp. Format is YYYY-MM-DDTHH:MM:SSZ.
739    /// Should be synced with the label io.container.image.created.
740    pub created: Option<String>,
741    /// Top level labels, to be prefixed to the ones with --label
742    /// Applied to both the outer config annotations and the inner config labels.
743    pub labels: Option<BTreeMap<String, String>>,
744    /// The output layers ordered. Provided as an ordered mapping of a unique
745    /// machine readable strings to a human readable name (e.g., the layer contents).
746    /// The human-readable name is placed in a layer annotation.
747    pub layers: IndexMap<String, String>,
748    /// The layer contents. The key is an ostree hash and the value is the
749    /// machine readable string of the layer the hash belongs to.
750    /// WARNING: needs to contain all ostree hashes in the input commit.
751    pub mapping: IndexMap<String, String>,
752    /// Whether the mapping is ordered. If true, the output tar stream of the
753    /// layers will reflect the order of the hashes in the mapping.
754    /// Otherwise, a deterministic ordering will be used regardless of mapping
755    /// order. Potentially useful for optimizing zstd:chunked compression.
756    /// WARNING: not currently supported.
757    pub ordered: Option<bool>,
758}
759
760/// Export a container image with an encapsulated ostree commit.
761#[allow(clippy::too_many_arguments)]
762async fn container_export(
763    repo: &ostree::Repo,
764    rev: &str,
765    imgref: &ImageReference,
766    labels: BTreeMap<String, String>,
767    authfile: Option<PathBuf>,
768    copy_meta_keys: Vec<String>,
769    copy_meta_opt_keys: Vec<String>,
770    container_config: Option<Utf8PathBuf>,
771    cmd: Option<Vec<String>>,
772    compression_fast: bool,
773    package_contentmeta: Option<Utf8PathBuf>,
774) -> Result<()> {
775    let container_config = if let Some(container_config) = container_config {
776        serde_json::from_reader(File::open(container_config).map(BufReader::new)?)?
777    } else {
778        None
779    };
780
781    let mut contentmeta_data = None;
782    let mut created = None;
783    let mut labels = labels.clone();
784    if let Some(contentmeta) = package_contentmeta {
785        let buf = File::open(contentmeta).map(BufReader::new);
786        let raw: RawMeta = serde_json::from_reader(buf?)?;
787
788        // Check future variables are set correctly
789        let supported_version = 1;
790        if raw.version != supported_version {
791            return Err(anyhow::anyhow!(
792                "Unsupported metadata version: {}. Currently supported: {}",
793                raw.version,
794                supported_version
795            ));
796        }
797        if let Some(ordered) = raw.ordered {
798            if ordered {
799                return Err(anyhow::anyhow!("Ordered mapping not currently supported."));
800            }
801        }
802
803        created = raw.created;
804        contentmeta_data = Some(ObjectMetaSized {
805            map: raw
806                .mapping
807                .into_iter()
808                .map(|(k, v)| (k, v.into()))
809                .collect(),
810            sizes: raw
811                .layers
812                .into_iter()
813                .map(|(k, v)| ObjectSourceMetaSized {
814                    meta: ObjectSourceMeta {
815                        identifier: k.clone().into(),
816                        name: v.into(),
817                        srcid: k.clone().into(),
818                        change_frequency: if k == "unpackaged" { u32::MAX } else { 1 },
819                        change_time_offset: 1,
820                    },
821                    size: 1,
822                })
823                .collect(),
824        });
825
826        // Merge --label args to the labels from the metadata
827        labels.extend(raw.labels.into_iter().flatten());
828    }
829
830    // Use enough layers so that each package ends in its own layer
831    // while respecting the layer ordering.
832    let max_layers = if let Some(contentmeta_data) = &contentmeta_data {
833        NonZeroU32::new((contentmeta_data.sizes.len() + 1).try_into().unwrap())
834    } else {
835        None
836    };
837
838    let config = Config {
839        labels: Some(labels),
840        cmd,
841    };
842
843    let opts = crate::container::ExportOpts {
844        copy_meta_keys,
845        copy_meta_opt_keys,
846        container_config,
847        authfile,
848        skip_compression: compression_fast, // TODO rename this in the struct at the next semver break
849        package_contentmeta: contentmeta_data.as_ref(),
850        max_layers,
851        created,
852        ..Default::default()
853    };
854    let pushed = crate::container::encapsulate(repo, rev, &config, Some(opts), imgref).await?;
855    println!("{pushed}");
856    Ok(())
857}
858
859/// Load metadata for a container image with an encapsulated ostree commit.
860async fn container_info(imgref: &OstreeImageReference) -> Result<()> {
861    let (_, digest) = crate::container::fetch_manifest(imgref).await?;
862    println!("{imgref} digest: {digest}");
863    Ok(())
864}
865
866/// Write a layered container image into an OSTree commit.
867async fn container_store(
868    repo: &ostree::Repo,
869    imgref: &OstreeImageReference,
870    ostree_digestfile: Option<Utf8PathBuf>,
871    proxyopts: ContainerProxyOpts,
872    quiet: bool,
873    check: Option<Utf8PathBuf>,
874) -> Result<()> {
875    let mut imp = ImageImporter::new(repo, imgref, proxyopts.into()).await?;
876    let prep = match imp.prepare().await? {
877        PrepareResult::AlreadyPresent(c) => {
878            write_digest_file(ostree_digestfile, &c.merge_commit)?;
879            println!("No changes in {} => {}", imgref, c.merge_commit);
880            return Ok(());
881        }
882        PrepareResult::Ready(r) => r,
883    };
884    if let Some(warning) = prep.deprecated_warning() {
885        print_deprecated_warning(warning).await;
886    }
887    if let Some(check) = check.as_deref() {
888        let rootfs = Dir::open_ambient_dir("/", cap_std::ambient_authority())?;
889        rootfs.atomic_replace_with(check.as_str().trim_start_matches('/'), |w| {
890            prep.manifest
891                .to_canon_json_writer(w)
892                .context("Serializing manifest")
893        })?;
894        // In check mode, we're done
895        return Ok(());
896    }
897    if let Some(previous_state) = prep.previous_state.as_ref() {
898        let diff = ManifestDiff::new(&previous_state.manifest, &prep.manifest);
899        diff.print();
900    }
901    print_layer_status(&prep);
902    let printer = (!quiet).then(|| {
903        let layer_progress = imp.request_progress();
904        let layer_byte_progress = imp.request_layer_progress();
905        tokio::task::spawn(async move {
906            handle_layer_progress_print(layer_progress, layer_byte_progress).await
907        })
908    });
909    let import = imp.import(prep).await;
910    if let Some(printer) = printer {
911        let _ = printer.await;
912    }
913    let import = import?;
914    if let Some(msg) =
915        ostree_container::store::image_filtered_content_warning(&import.filtered_files)?
916    {
917        eprintln!("{msg}")
918    }
919    if let Some(ref text) = import.verify_text {
920        println!("{text}");
921    }
922    write_digest_file(ostree_digestfile, &import.merge_commit)?;
923    println!("Wrote: {} => {}", imgref, import.merge_commit);
924    Ok(())
925}
926
927fn write_digest_file(digestfile: Option<Utf8PathBuf>, digest: &str) -> Result<()> {
928    if let Some(digestfile) = digestfile.as_deref() {
929        let rootfs = Dir::open_ambient_dir("/", cap_std::ambient_authority())?;
930        rootfs.write(digestfile.as_str().trim_start_matches('/'), digest)?;
931    }
932    Ok(())
933}
934
935/// Output the container image history
936async fn container_history(repo: &ostree::Repo, imgref: &ImageReference) -> Result<()> {
937    let img = crate::container::store::query_image(repo, imgref)?
938        .ok_or_else(|| anyhow::anyhow!("No such image: {}", imgref))?;
939    let mut table = comfy_table::Table::new();
940    table
941        .load_preset(comfy_table::presets::NOTHING)
942        .set_content_arrangement(comfy_table::ContentArrangement::Dynamic)
943        .set_header(["ID", "SIZE", "CRCEATED BY"]);
944
945    let mut history = img.configuration.history().iter().flatten();
946    let layers = img.manifest.layers().iter();
947    for layer in layers {
948        let histent = history.next();
949        let created_by = histent
950            .and_then(|s| s.created_by().as_deref())
951            .unwrap_or("");
952
953        let digest = layer.digest().digest();
954        // Verify it's OK to slice, this should all be ASCII
955        assert!(digest.is_ascii());
956        let digest_max = 20usize;
957        let digest = &digest[0..digest_max];
958        let size = glib::format_size(layer.size());
959        table.add_row([digest, size.as_str(), created_by]);
960    }
961    println!("{table}");
962    Ok(())
963}
964
965/// Add IMA signatures to an ostree commit, generating a new commit.
966fn ima_sign(cmdopts: &ImaSignOpts) -> Result<()> {
967    let cancellable = gio::Cancellable::NONE;
968    let signopts = crate::ima::ImaOpts {
969        algorithm: cmdopts.algorithm.clone(),
970        key: cmdopts.key.clone(),
971        overwrite: cmdopts.overwrite,
972    };
973    let repo = parse_repo(&cmdopts.repo)?;
974    let tx = repo.auto_transaction(cancellable)?;
975    let signed_commit = crate::ima::ima_sign(&repo, cmdopts.src_rev.as_str(), &signopts)?;
976    repo.transaction_set_ref(
977        None,
978        cmdopts.target_ref.as_str(),
979        Some(signed_commit.as_str()),
980    );
981    let _stats = tx.commit(cancellable)?;
982    println!("{} => {}", cmdopts.target_ref, signed_commit);
983    Ok(())
984}
985
986#[cfg(feature = "internal-testing-api")]
987async fn testing(opts: &TestingOpts) -> Result<()> {
988    match opts {
989        TestingOpts::DetectEnv => {
990            println!("{}", crate::integrationtest::detectenv()?);
991            Ok(())
992        }
993        TestingOpts::CreateFixture => crate::integrationtest::create_fixture().await,
994        TestingOpts::Run => crate::integrationtest::run_tests(),
995        TestingOpts::RunIMA => crate::integrationtest::test_ima(),
996        TestingOpts::FilterTar => {
997            let tmpdir = cap_std_ext::cap_tempfile::TempDir::new(cap_std::ambient_authority())?;
998            crate::tar::filter_tar(
999                std::io::stdin(),
1000                std::io::stdout(),
1001                &Default::default(),
1002                &tmpdir,
1003            )
1004            .map(|_| {})
1005        }
1006    }
1007}
1008
1009// Quick hack; TODO dedup this with the code in bootc or lower here
1010#[context("Remounting sysroot writable")]
1011fn container_remount_sysroot(sysroot: &Utf8Path) -> Result<()> {
1012    if !Utf8Path::new("/run/.containerenv").exists() {
1013        return Ok(());
1014    }
1015    println!("Running in container, assuming we can remount {sysroot} writable");
1016    let st = Command::new("mount")
1017        .args(["-o", "remount,rw", sysroot.as_str()])
1018        .status()?;
1019    if !st.success() {
1020        anyhow::bail!("Failed to remount {sysroot}: {st:?}");
1021    }
1022    Ok(())
1023}
1024
1025#[context("Serializing to output file")]
1026fn handle_serialize_to_file<T: serde::Serialize>(path: Option<&Utf8Path>, obj: T) -> Result<()> {
1027    if let Some(path) = path {
1028        let mut out = std::fs::File::create(path)
1029            .map(BufWriter::new)
1030            .with_context(|| anyhow::anyhow!("Opening {path} for writing"))?;
1031        obj.to_canon_json_writer(&mut out)
1032            .context("Serializing output")?;
1033    }
1034    Ok(())
1035}
1036
1037/// Parse the provided arguments and execute.
1038/// Calls [`clap::Error::exit`] on failure, printing the error message and aborting the program.
1039pub async fn run_from_iter<I>(args: I) -> Result<()>
1040where
1041    I: IntoIterator,
1042    I::Item: Into<OsString> + Clone,
1043{
1044    run_from_opt(Opt::parse_from(args)).await
1045}
1046
1047async fn run_from_opt(opt: Opt) -> Result<()> {
1048    match opt {
1049        Opt::Tar(TarOpts::Import(ref opt)) => tar_import(opt).await,
1050        Opt::Tar(TarOpts::Export(ref opt)) => tar_export(opt),
1051        Opt::Container(o) => match o {
1052            ContainerOpts::Info { imgref } => container_info(&imgref).await,
1053            ContainerOpts::Commit => container_commit().await,
1054            ContainerOpts::Unencapsulate {
1055                repo,
1056                imgref,
1057                proxyopts,
1058                write_ref,
1059                quiet,
1060            } => {
1061                let repo = parse_repo(&repo)?;
1062                container_import(&repo, &imgref, proxyopts, write_ref.as_deref(), quiet).await
1063            }
1064            ContainerOpts::Encapsulate {
1065                repo,
1066                rev,
1067                imgref,
1068                labels,
1069                authfile,
1070                copy_meta_keys,
1071                copy_meta_opt_keys,
1072                config,
1073                cmd,
1074                compression_fast,
1075                contentmeta,
1076            } => {
1077                let labels: Result<BTreeMap<_, _>> = labels
1078                    .into_iter()
1079                    .map(|l| {
1080                        let (k, v) = l
1081                            .split_once('=')
1082                            .ok_or_else(|| anyhow::anyhow!("Missing '=' in label {}", l))?;
1083                        Ok((k.to_string(), v.to_string()))
1084                    })
1085                    .collect();
1086                let repo = parse_repo(&repo)?;
1087                container_export(
1088                    &repo,
1089                    &rev,
1090                    &imgref,
1091                    labels?,
1092                    authfile,
1093                    copy_meta_keys,
1094                    copy_meta_opt_keys,
1095                    config,
1096                    cmd,
1097                    compression_fast,
1098                    contentmeta,
1099                )
1100                .await
1101            }
1102            ContainerOpts::Image(opts) => match opts {
1103                ContainerImageOpts::List { repo } => {
1104                    let repo = parse_repo(&repo)?;
1105                    for image in crate::container::store::list_images(&repo)? {
1106                        println!("{image}");
1107                    }
1108                    Ok(())
1109                }
1110                ContainerImageOpts::Pull {
1111                    repo,
1112                    imgref,
1113                    ostree_digestfile,
1114                    proxyopts,
1115                    quiet,
1116                    check,
1117                } => {
1118                    let repo = parse_repo(&repo)?;
1119                    container_store(&repo, &imgref, ostree_digestfile, proxyopts, quiet, check)
1120                        .await
1121                }
1122                ContainerImageOpts::Reexport {
1123                    repo,
1124                    src_imgref,
1125                    dest_imgref,
1126                    authfile,
1127                    compression_fast,
1128                } => {
1129                    let repo = &parse_repo(&repo)?;
1130                    let opts = ExportToOCIOpts {
1131                        authfile,
1132                        skip_compression: compression_fast,
1133                        ..Default::default()
1134                    };
1135                    let digest = ostree_container::store::export(
1136                        repo,
1137                        &src_imgref,
1138                        &dest_imgref,
1139                        Some(opts),
1140                    )
1141                    .await?;
1142                    println!("Exported: {digest}");
1143                    Ok(())
1144                }
1145                ContainerImageOpts::History { repo, imgref } => {
1146                    let repo = parse_repo(&repo)?;
1147                    container_history(&repo, &imgref).await
1148                }
1149                ContainerImageOpts::Metadata {
1150                    repo,
1151                    imgref,
1152                    config,
1153                } => {
1154                    let repo = parse_repo(&repo)?;
1155                    let image = crate::container::store::query_image(&repo, &imgref)?
1156                        .ok_or_else(|| anyhow::anyhow!("No such image"))?;
1157                    let stdout = std::io::stdout().lock();
1158                    let mut stdout = std::io::BufWriter::new(stdout);
1159                    if config {
1160                        image.configuration.to_canon_json_writer(&mut stdout)?;
1161                    } else {
1162                        image.manifest.to_canon_json_writer(&mut stdout)?;
1163                    }
1164                    stdout.flush()?;
1165                    Ok(())
1166                }
1167                ContainerImageOpts::ClearCachedUpdate { repo, imgref } => {
1168                    let repo = parse_repo(&repo)?;
1169                    crate::container::store::clear_cached_update(&repo, &imgref)?;
1170                    Ok(())
1171                }
1172                ContainerImageOpts::Remove {
1173                    repo,
1174                    imgrefs,
1175                    skip_gc,
1176                } => {
1177                    let nimgs = imgrefs.len();
1178                    let repo = parse_repo(&repo)?;
1179                    crate::container::store::remove_images(&repo, imgrefs.iter())?;
1180                    if !skip_gc {
1181                        let nlayers = crate::container::store::gc_image_layers(&repo)?;
1182                        println!("Removed images: {nimgs} layers: {nlayers}");
1183                    } else {
1184                        println!("Removed images: {nimgs}");
1185                    }
1186                    Ok(())
1187                }
1188                ContainerImageOpts::PruneLayers { repo } => {
1189                    let repo = parse_repo(&repo)?;
1190                    let nlayers = crate::container::store::gc_image_layers(&repo)?;
1191                    println!("Removed layers: {nlayers}");
1192                    Ok(())
1193                }
1194                ContainerImageOpts::PruneImages {
1195                    sysroot,
1196                    and_layers,
1197                    full,
1198                } => {
1199                    let sysroot = &ostree::Sysroot::new(Some(&gio::File::for_path(&sysroot)));
1200                    sysroot.load(gio::Cancellable::NONE)?;
1201                    let sysroot = &SysrootLock::new_from_sysroot(sysroot).await?;
1202                    if full {
1203                        let res = crate::container::deploy::prune(sysroot)?;
1204                        if res.is_empty() {
1205                            println!("No content was pruned.");
1206                        } else {
1207                            println!("Removed images: {}", res.n_images);
1208                            println!("Removed layers: {}", res.n_layers);
1209                            println!("Removed objects: {}", res.n_objects_pruned);
1210                            let objsize = glib::format_size(res.objsize);
1211                            println!("Freed: {objsize}");
1212                        }
1213                    } else {
1214                        let removed = crate::container::deploy::remove_undeployed_images(sysroot)?;
1215                        match removed.as_slice() {
1216                            [] => {
1217                                println!("No unreferenced images.");
1218                                return Ok(());
1219                            }
1220                            o => {
1221                                for imgref in o {
1222                                    println!("Removed: {imgref}");
1223                                }
1224                            }
1225                        }
1226                        if and_layers {
1227                            let nlayers =
1228                                crate::container::store::gc_image_layers(&sysroot.repo())?;
1229                            println!("Removed layers: {nlayers}");
1230                        }
1231                    }
1232                    Ok(())
1233                }
1234                ContainerImageOpts::Copy {
1235                    src_repo,
1236                    dest_repo,
1237                    imgref,
1238                } => {
1239                    let src_repo = parse_repo(&src_repo)?;
1240                    let dest_repo = parse_repo(&dest_repo)?;
1241                    let imgref = &imgref.imgref;
1242                    crate::container::store::copy(&src_repo, imgref, &dest_repo, imgref).await
1243                }
1244                ContainerImageOpts::ReplaceDetachedMetadata {
1245                    src,
1246                    dest,
1247                    contents,
1248                } => {
1249                    let contents = contents.map(std::fs::read).transpose()?;
1250                    let digest = crate::container::update_detached_metadata(
1251                        &src,
1252                        &dest,
1253                        contents.as_deref(),
1254                    )
1255                    .await?;
1256                    println!("Pushed: {digest}");
1257                    Ok(())
1258                }
1259                ContainerImageOpts::Deploy {
1260                    sysroot,
1261                    stateroot,
1262                    imgref,
1263                    image,
1264                    transport,
1265                    no_signature_verification: _,
1266                    enforce_container_sigpolicy,
1267                    ostree_remote,
1268                    target_imgref,
1269                    no_imgref,
1270                    karg,
1271                    proxyopts,
1272                    write_commitid_to,
1273                } => {
1274                    // As of recent releases, signature verification enforcement is
1275                    // off by default, and must be explicitly enabled.
1276                    let no_signature_verification = !enforce_container_sigpolicy;
1277                    let sysroot = &if let Some(sysroot) = sysroot {
1278                        ostree::Sysroot::new(Some(&gio::File::for_path(sysroot)))
1279                    } else {
1280                        ostree::Sysroot::new_default()
1281                    };
1282                    sysroot.load(gio::Cancellable::NONE)?;
1283                    let kargs = karg.as_deref();
1284                    let kargs = kargs.map(|v| {
1285                        let r: Vec<_> = v.iter().map(|s| s.as_str()).collect();
1286                        r
1287                    });
1288
1289                    // If the user specified a stateroot, we always use that.
1290                    let stateroot = if let Some(stateroot) = stateroot.as_deref() {
1291                        Cow::Borrowed(stateroot)
1292                    } else {
1293                        // Otherwise, if we're booted via ostree, use the booted.
1294                        // If that doesn't hold, then use `default`.
1295                        let booted_stateroot = sysroot
1296                            .booted_deployment()
1297                            .map(|d| Cow::Owned(d.osname().to_string()));
1298                        booted_stateroot.unwrap_or({
1299                            Cow::Borrowed(crate::container::deploy::STATEROOT_DEFAULT)
1300                        })
1301                    };
1302
1303                    let imgref = if let Some(image) = image {
1304                        let transport = transport.as_deref().unwrap_or("registry");
1305                        let transport = ostree_container::Transport::try_from(transport)?;
1306                        let imgref = ostree_container::ImageReference {
1307                            transport,
1308                            name: image,
1309                        };
1310                        let sigverify = if no_signature_verification {
1311                            ostree_container::SignatureSource::ContainerPolicyAllowInsecure
1312                        } else if let Some(remote) = ostree_remote.as_ref() {
1313                            ostree_container::SignatureSource::OstreeRemote(remote.to_string())
1314                        } else {
1315                            ostree_container::SignatureSource::ContainerPolicy
1316                        };
1317                        ostree_container::OstreeImageReference { sigverify, imgref }
1318                    } else {
1319                        // SAFETY: We use the clap required_unless_present flag, so this must be set
1320                        // because --image is not.
1321                        let imgref = imgref.expect("imgref option should be set");
1322                        imgref.as_str().try_into()?
1323                    };
1324
1325                    #[allow(clippy::needless_update)]
1326                    let options = crate::container::deploy::DeployOpts {
1327                        kargs: kargs.as_deref(),
1328                        target_imgref: target_imgref.as_ref(),
1329                        proxy_cfg: Some(proxyopts.into()),
1330                        no_imgref,
1331                        ..Default::default()
1332                    };
1333                    let state = crate::container::deploy::deploy(
1334                        sysroot,
1335                        &stateroot,
1336                        &imgref,
1337                        Some(options),
1338                    )
1339                    .await?;
1340                    if let Some(msg) = ostree_container::store::image_filtered_content_warning(
1341                        &state.filtered_files,
1342                    )? {
1343                        eprintln!("{msg}")
1344                    }
1345                    if let Some(p) = write_commitid_to {
1346                        std::fs::write(&p, state.merge_commit.as_bytes())
1347                            .with_context(|| format!("Failed to write commitid to {p}"))?;
1348                    }
1349                    Ok(())
1350                }
1351            },
1352            ContainerOpts::Compare {
1353                imgref_old,
1354                imgref_new,
1355            } => {
1356                let (manifest_old, _) = crate::container::fetch_manifest(&imgref_old).await?;
1357                let (manifest_new, _) = crate::container::fetch_manifest(&imgref_new).await?;
1358                let manifest_diff =
1359                    crate::container::ManifestDiff::new(&manifest_old, &manifest_new);
1360                manifest_diff.print();
1361                Ok(())
1362            }
1363        },
1364        Opt::ImaSign(ref opts) => ima_sign(opts),
1365        #[cfg(feature = "internal-testing-api")]
1366        Opt::InternalOnlyForTesting(ref opts) => testing(opts).await,
1367        #[cfg(feature = "docgen")]
1368        Opt::Man(manopts) => crate::docgen::generate_manpages(&manopts.directory),
1369        Opt::ProvisionalRepair(opts) => match opts {
1370            ProvisionalRepairOpts::AnalyzeInodes {
1371                repo,
1372                verbose,
1373                write_result_to,
1374            } => {
1375                let repo = parse_repo(&repo)?;
1376                let check_res = crate::repair::check_inode_collision(&repo, verbose)?;
1377                handle_serialize_to_file(write_result_to.as_deref(), &check_res)?;
1378                if check_res.collisions.is_empty() {
1379                    println!("OK: No colliding objects found.");
1380                } else {
1381                    eprintln!(
1382                        "warning: {} potentially colliding inodes found",
1383                        check_res.collisions.len()
1384                    );
1385                }
1386                Ok(())
1387            }
1388            ProvisionalRepairOpts::Repair {
1389                sysroot,
1390                verbose,
1391                dry_run,
1392                write_result_to,
1393            } => {
1394                container_remount_sysroot(&sysroot)?;
1395                let sysroot = &ostree::Sysroot::new(Some(&gio::File::for_path(&sysroot)));
1396                sysroot.load(gio::Cancellable::NONE)?;
1397                let sysroot = &SysrootLock::new_from_sysroot(sysroot).await?;
1398                let result = crate::repair::analyze_for_repair(sysroot, verbose)?;
1399                handle_serialize_to_file(write_result_to.as_deref(), &result)?;
1400                if dry_run {
1401                    result.check()
1402                } else {
1403                    result.repair(sysroot)
1404                }
1405            }
1406        },
1407    }
1408}