Skip to main content

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        let mut c = ostree_container::store::ImageProxyConfig::default();
570        c.auth_anonymous = self.auth_anonymous;
571        c.authfile = self.authfile;
572        c.certificate_directory = self.cert_dir;
573        c.insecure_skip_tls_verification = Some(self.insecure_skip_tls_verification);
574        c
575    }
576}
577
578/// Import a tar archive containing an ostree commit.
579async fn tar_import(opts: &ImportOpts) -> Result<()> {
580    let repo = parse_repo(&opts.repo)?;
581    let imported = if let Some(path) = opts.path.as_ref() {
582        let instream = tokio::fs::File::open(path).await?;
583        crate::tar::import_tar(&repo, instream, None).await?
584    } else {
585        let stdin = tokio::io::stdin();
586        crate::tar::import_tar(&repo, stdin, None).await?
587    };
588    println!("Imported: {imported}");
589    Ok(())
590}
591
592/// Export a tar archive containing an ostree commit.
593fn tar_export(opts: &ExportOpts) -> Result<()> {
594    let repo = parse_repo(&opts.repo)?;
595    #[allow(clippy::needless_update)]
596    let subopts = crate::tar::ExportOptions {
597        ..Default::default()
598    };
599    crate::tar::export_commit(&repo, opts.rev.as_str(), std::io::stdout(), Some(subopts))?;
600    Ok(())
601}
602
603/// Render an import progress notification as a string.
604pub fn layer_progress_format(p: &ImportProgress) -> String {
605    let (starting, s, layer) = match p {
606        ImportProgress::OstreeChunkStarted(v) => (true, "ostree chunk", v),
607        ImportProgress::OstreeChunkCompleted(v) => (false, "ostree chunk", v),
608        ImportProgress::DerivedLayerStarted(v) => (true, "layer", v),
609        ImportProgress::DerivedLayerCompleted(v) => (false, "layer", v),
610    };
611    // podman outputs 12 characters of digest, let's add 7 for `sha256:`.
612    let short_digest = layer
613        .digest()
614        .digest()
615        .chars()
616        .take(12 + 7)
617        .collect::<String>();
618    if starting {
619        let size = glib::format_size(layer.size());
620        format!("Fetching {s} {short_digest} ({size})")
621    } else {
622        format!("Fetched {s} {short_digest}")
623    }
624}
625
626/// Write container fetch progress to standard output.
627pub async fn handle_layer_progress_print(
628    mut layers: Receiver<ImportProgress>,
629    mut layer_bytes: tokio::sync::watch::Receiver<Option<LayerProgress>>,
630) {
631    let style = indicatif::ProgressStyle::default_bar();
632    let pb = indicatif::ProgressBar::new(100);
633    pb.set_style(
634        style
635            .template("{prefix} {bytes} [{bar:20}] ({eta}) {msg}")
636            .unwrap(),
637    );
638    loop {
639        tokio::select! {
640            // Always handle layer changes first.
641            biased;
642            layer = layers.recv() => {
643                if let Some(l) = layer {
644                    if l.is_starting() {
645                        pb.set_position(0);
646                    } else {
647                        pb.finish();
648                    }
649                    pb.set_message(layer_progress_format(&l));
650                } else {
651                    // If the receiver is disconnected, then we're done
652                    break
653                };
654            },
655            r = layer_bytes.changed() => {
656                if r.is_err() {
657                    // If the receiver is disconnected, then we're done
658                    break
659                }
660                let bytes = layer_bytes.borrow();
661                if let Some(bytes) = &*bytes {
662                    pb.set_length(bytes.total);
663                    pb.set_position(bytes.fetched);
664                }
665            }
666
667        }
668    }
669}
670
671/// Write the status of layers to download.
672pub fn print_layer_status(prep: &PreparedImport) {
673    if let Some(status) = prep.format_layer_status() {
674        println!("{status}");
675        let _ = std::io::stdout().flush();
676    }
677}
678
679/// Write a deprecation notice, and sleep for 3 seconds.
680pub async fn print_deprecated_warning(msg: &str) {
681    eprintln!("warning: {msg}");
682    tokio::time::sleep(std::time::Duration::from_secs(3)).await
683}
684
685/// Import a container image with an encapsulated ostree commit.
686async fn container_import(
687    repo: &ostree::Repo,
688    imgref: &OstreeImageReference,
689    proxyopts: ContainerProxyOpts,
690    write_ref: Option<&str>,
691    quiet: bool,
692) -> Result<()> {
693    let target = indicatif::ProgressDrawTarget::stdout();
694    let style = indicatif::ProgressStyle::default_bar();
695    let pb = (!quiet).then(|| {
696        let pb = indicatif::ProgressBar::new_spinner();
697        pb.set_draw_target(target);
698        pb.set_style(style.template("{spinner} {prefix} {msg}").unwrap());
699        pb.enable_steady_tick(std::time::Duration::from_millis(200));
700        pb.set_message("Downloading...");
701        pb
702    });
703    let importer = ImageImporter::new(repo, imgref, proxyopts.into()).await?;
704    let import = importer.unencapsulate().await;
705    // Ensure we finish the progress bar before potentially propagating an error
706    if let Some(pb) = pb.as_ref() {
707        pb.finish();
708    }
709    let import = import?;
710    if let Some(warning) = import.deprecated_warning.as_deref() {
711        print_deprecated_warning(warning).await;
712    }
713    if let Some(write_ref) = write_ref {
714        repo.set_ref_immediate(
715            None,
716            write_ref,
717            Some(import.ostree_commit.as_str()),
718            gio::Cancellable::NONE,
719        )?;
720        println!(
721            "Imported: {} => {}",
722            write_ref,
723            import.ostree_commit.as_str()
724        );
725    } else {
726        println!("Imported: {}", import.ostree_commit);
727    }
728
729    Ok(())
730}
731
732/// Grouping of metadata about an object.
733#[derive(Debug, Default, Serialize, Deserialize)]
734pub struct RawMeta {
735    /// The metadata format version. Should be set to 1.
736    pub version: u32,
737    /// The image creation timestamp. Format is YYYY-MM-DDTHH:MM:SSZ.
738    /// Should be synced with the label io.container.image.created.
739    pub created: Option<String>,
740    /// Top level labels, to be prefixed to the ones with --label
741    /// Applied to both the outer config annotations and the inner config labels.
742    pub labels: Option<BTreeMap<String, String>>,
743    /// The output layers ordered. Provided as an ordered mapping of a unique
744    /// machine readable strings to a human readable name (e.g., the layer contents).
745    /// The human-readable name is placed in a layer annotation.
746    pub layers: IndexMap<String, String>,
747    /// The layer contents. The key is an ostree hash and the value is the
748    /// machine readable string of the layer the hash belongs to.
749    /// WARNING: needs to contain all ostree hashes in the input commit.
750    pub mapping: IndexMap<String, String>,
751    /// Whether the mapping is ordered. If true, the output tar stream of the
752    /// layers will reflect the order of the hashes in the mapping.
753    /// Otherwise, a deterministic ordering will be used regardless of mapping
754    /// order. Potentially useful for optimizing zstd:chunked compression.
755    /// WARNING: not currently supported.
756    pub ordered: Option<bool>,
757}
758
759/// Export a container image with an encapsulated ostree commit.
760#[allow(clippy::too_many_arguments)]
761async fn container_export(
762    repo: &ostree::Repo,
763    rev: &str,
764    imgref: &ImageReference,
765    labels: BTreeMap<String, String>,
766    authfile: Option<PathBuf>,
767    copy_meta_keys: Vec<String>,
768    copy_meta_opt_keys: Vec<String>,
769    container_config: Option<Utf8PathBuf>,
770    cmd: Option<Vec<String>>,
771    compression_fast: bool,
772    package_contentmeta: Option<Utf8PathBuf>,
773) -> Result<()> {
774    let container_config = if let Some(container_config) = container_config {
775        serde_json::from_reader(File::open(container_config).map(BufReader::new)?)?
776    } else {
777        None
778    };
779
780    let mut contentmeta_data = None;
781    let mut created = None;
782    let mut labels = labels.clone();
783    if let Some(contentmeta) = package_contentmeta {
784        let buf = File::open(contentmeta).map(BufReader::new);
785        let raw: RawMeta = serde_json::from_reader(buf?)?;
786
787        // Check future variables are set correctly
788        let supported_version = 1;
789        if raw.version != supported_version {
790            return Err(anyhow::anyhow!(
791                "Unsupported metadata version: {}. Currently supported: {}",
792                raw.version,
793                supported_version
794            ));
795        }
796        if let Some(ordered) = raw.ordered {
797            if ordered {
798                return Err(anyhow::anyhow!("Ordered mapping not currently supported."));
799            }
800        }
801
802        created = raw.created;
803        contentmeta_data = Some(ObjectMetaSized {
804            map: raw
805                .mapping
806                .into_iter()
807                .map(|(k, v)| (k, v.into()))
808                .collect(),
809            sizes: raw
810                .layers
811                .into_iter()
812                .map(|(k, v)| ObjectSourceMetaSized {
813                    meta: ObjectSourceMeta {
814                        identifier: k.clone().into(),
815                        name: v.into(),
816                        srcid: k.clone().into(),
817                        change_frequency: if k == "unpackaged" { u32::MAX } else { 1 },
818                        change_time_offset: 1,
819                    },
820                    size: 1,
821                })
822                .collect(),
823        });
824
825        // Merge --label args to the labels from the metadata
826        labels.extend(raw.labels.into_iter().flatten());
827    }
828
829    // Use enough layers so that each package ends in its own layer
830    // while respecting the layer ordering.
831    let max_layers = if let Some(contentmeta_data) = &contentmeta_data {
832        NonZeroU32::new((contentmeta_data.sizes.len() + 1).try_into().unwrap())
833    } else {
834        None
835    };
836
837    let config = Config {
838        labels: Some(labels),
839        cmd,
840    };
841
842    let opts = crate::container::ExportOpts {
843        copy_meta_keys,
844        copy_meta_opt_keys,
845        container_config,
846        authfile,
847        skip_compression: compression_fast, // TODO rename this in the struct at the next semver break
848        package_contentmeta: contentmeta_data.as_ref(),
849        max_layers,
850        created,
851        ..Default::default()
852    };
853    let pushed = crate::container::encapsulate(repo, rev, &config, Some(opts), imgref).await?;
854    println!("{pushed}");
855    Ok(())
856}
857
858/// Load metadata for a container image with an encapsulated ostree commit.
859async fn container_info(imgref: &OstreeImageReference) -> Result<()> {
860    let (_, digest) = crate::container::fetch_manifest(imgref).await?;
861    println!("{imgref} digest: {digest}");
862    Ok(())
863}
864
865/// Write a layered container image into an OSTree commit.
866async fn container_store(
867    repo: &ostree::Repo,
868    imgref: &OstreeImageReference,
869    ostree_digestfile: Option<Utf8PathBuf>,
870    proxyopts: ContainerProxyOpts,
871    quiet: bool,
872    check: Option<Utf8PathBuf>,
873) -> Result<()> {
874    let mut imp = ImageImporter::new(repo, imgref, proxyopts.into()).await?;
875    let prep = match imp.prepare().await? {
876        PrepareResult::AlreadyPresent(c) => {
877            write_digest_file(ostree_digestfile, &c.merge_commit)?;
878            println!("No changes in {} => {}", imgref, c.merge_commit);
879            return Ok(());
880        }
881        PrepareResult::Ready(r) => r,
882    };
883    if let Some(warning) = prep.deprecated_warning() {
884        print_deprecated_warning(warning).await;
885    }
886    if let Some(check) = check.as_deref() {
887        let rootfs = Dir::open_ambient_dir("/", cap_std::ambient_authority())?;
888        rootfs.atomic_replace_with(check.as_str().trim_start_matches('/'), |w| {
889            prep.manifest
890                .to_canon_json_writer(w)
891                .context("Serializing manifest")
892        })?;
893        // In check mode, we're done
894        return Ok(());
895    }
896    if let Some(previous_state) = prep.previous_state.as_ref() {
897        let diff = ManifestDiff::new(&previous_state.manifest, &prep.manifest);
898        diff.print();
899    }
900    print_layer_status(&prep);
901    let printer = (!quiet).then(|| {
902        let layer_progress = imp.request_progress();
903        let layer_byte_progress = imp.request_layer_progress();
904        tokio::task::spawn(async move {
905            handle_layer_progress_print(layer_progress, layer_byte_progress).await
906        })
907    });
908    let import = imp.import(prep).await;
909    if let Some(printer) = printer {
910        let _ = printer.await;
911    }
912    let import = import?;
913    if let Some(msg) =
914        ostree_container::store::image_filtered_content_warning(&import.filtered_files)?
915    {
916        eprintln!("{msg}")
917    }
918    if let Some(ref text) = import.verify_text {
919        println!("{text}");
920    }
921    write_digest_file(ostree_digestfile, &import.merge_commit)?;
922    println!("Wrote: {} => {}", imgref, import.merge_commit);
923    Ok(())
924}
925
926fn write_digest_file(digestfile: Option<Utf8PathBuf>, digest: &str) -> Result<()> {
927    if let Some(digestfile) = digestfile.as_deref() {
928        let rootfs = Dir::open_ambient_dir("/", cap_std::ambient_authority())?;
929        rootfs.write(digestfile.as_str().trim_start_matches('/'), digest)?;
930    }
931    Ok(())
932}
933
934/// Output the container image history
935async fn container_history(repo: &ostree::Repo, imgref: &ImageReference) -> Result<()> {
936    let img = crate::container::store::query_image(repo, imgref)?
937        .ok_or_else(|| anyhow::anyhow!("No such image: {}", imgref))?;
938    let mut table = comfy_table::Table::new();
939    table
940        .load_preset(comfy_table::presets::NOTHING)
941        .set_content_arrangement(comfy_table::ContentArrangement::Dynamic)
942        .set_header(["ID", "SIZE", "CRCEATED BY"]);
943
944    let mut history = img.configuration.history().iter().flatten();
945    let layers = img.manifest.layers().iter();
946    for layer in layers {
947        let histent = history.next();
948        let created_by = histent
949            .and_then(|s| s.created_by().as_deref())
950            .unwrap_or("");
951
952        let digest = layer.digest().digest();
953        // Verify it's OK to slice, this should all be ASCII
954        assert!(digest.is_ascii());
955        let digest_max = 20usize;
956        let digest = &digest[0..digest_max];
957        let size = glib::format_size(layer.size());
958        table.add_row([digest, size.as_str(), created_by]);
959    }
960    println!("{table}");
961    Ok(())
962}
963
964/// Add IMA signatures to an ostree commit, generating a new commit.
965fn ima_sign(cmdopts: &ImaSignOpts) -> Result<()> {
966    let cancellable = gio::Cancellable::NONE;
967    let signopts = crate::ima::ImaOpts {
968        algorithm: cmdopts.algorithm.clone(),
969        key: cmdopts.key.clone(),
970        overwrite: cmdopts.overwrite,
971    };
972    let repo = parse_repo(&cmdopts.repo)?;
973    let tx = repo.auto_transaction(cancellable)?;
974    let signed_commit = crate::ima::ima_sign(&repo, cmdopts.src_rev.as_str(), &signopts)?;
975    repo.transaction_set_ref(
976        None,
977        cmdopts.target_ref.as_str(),
978        Some(signed_commit.as_str()),
979    );
980    let _stats = tx.commit(cancellable)?;
981    println!("{} => {}", cmdopts.target_ref, signed_commit);
982    Ok(())
983}
984
985#[cfg(feature = "internal-testing-api")]
986async fn testing(opts: &TestingOpts) -> Result<()> {
987    match opts {
988        TestingOpts::DetectEnv => {
989            println!("{}", crate::integrationtest::detectenv()?);
990            Ok(())
991        }
992        TestingOpts::CreateFixture => crate::integrationtest::create_fixture().await,
993        TestingOpts::Run => crate::integrationtest::run_tests(),
994        TestingOpts::RunIMA => crate::integrationtest::test_ima(),
995        TestingOpts::FilterTar => {
996            let tmpdir = cap_std_ext::cap_tempfile::TempDir::new(cap_std::ambient_authority())?;
997            crate::tar::filter_tar(
998                std::io::stdin(),
999                std::io::stdout(),
1000                &Default::default(),
1001                &tmpdir,
1002            )
1003            .map(|_| {})
1004        }
1005    }
1006}
1007
1008// Quick hack; TODO dedup this with the code in bootc or lower here
1009#[context("Remounting sysroot writable")]
1010fn container_remount_sysroot(sysroot: &Utf8Path) -> Result<()> {
1011    if !Utf8Path::new("/run/.containerenv").exists() {
1012        return Ok(());
1013    }
1014    println!("Running in container, assuming we can remount {sysroot} writable");
1015    let st = Command::new("mount")
1016        .args(["-o", "remount,rw", sysroot.as_str()])
1017        .status()?;
1018    if !st.success() {
1019        anyhow::bail!("Failed to remount {sysroot}: {st:?}");
1020    }
1021    Ok(())
1022}
1023
1024#[context("Serializing to output file")]
1025fn handle_serialize_to_file<T: serde::Serialize>(path: Option<&Utf8Path>, obj: T) -> Result<()> {
1026    if let Some(path) = path {
1027        let mut out = std::fs::File::create(path)
1028            .map(BufWriter::new)
1029            .with_context(|| anyhow::anyhow!("Opening {path} for writing"))?;
1030        obj.to_canon_json_writer(&mut out)
1031            .context("Serializing output")?;
1032    }
1033    Ok(())
1034}
1035
1036/// Parse the provided arguments and execute.
1037/// Calls [`clap::Error::exit`] on failure, printing the error message and aborting the program.
1038pub async fn run_from_iter<I>(args: I) -> Result<()>
1039where
1040    I: IntoIterator,
1041    I::Item: Into<OsString> + Clone,
1042{
1043    run_from_opt(Opt::parse_from(args)).await
1044}
1045
1046async fn run_from_opt(opt: Opt) -> Result<()> {
1047    match opt {
1048        Opt::Tar(TarOpts::Import(ref opt)) => tar_import(opt).await,
1049        Opt::Tar(TarOpts::Export(ref opt)) => tar_export(opt),
1050        Opt::Container(o) => match o {
1051            ContainerOpts::Info { imgref } => container_info(&imgref).await,
1052            ContainerOpts::Commit => container_commit().await,
1053            ContainerOpts::Unencapsulate {
1054                repo,
1055                imgref,
1056                proxyopts,
1057                write_ref,
1058                quiet,
1059            } => {
1060                let repo = parse_repo(&repo)?;
1061                container_import(&repo, &imgref, proxyopts, write_ref.as_deref(), quiet).await
1062            }
1063            ContainerOpts::Encapsulate {
1064                repo,
1065                rev,
1066                imgref,
1067                labels,
1068                authfile,
1069                copy_meta_keys,
1070                copy_meta_opt_keys,
1071                config,
1072                cmd,
1073                compression_fast,
1074                contentmeta,
1075            } => {
1076                let labels: Result<BTreeMap<_, _>> = labels
1077                    .into_iter()
1078                    .map(|l| {
1079                        let (k, v) = l
1080                            .split_once('=')
1081                            .ok_or_else(|| anyhow::anyhow!("Missing '=' in label {}", l))?;
1082                        Ok((k.to_string(), v.to_string()))
1083                    })
1084                    .collect();
1085                let repo = parse_repo(&repo)?;
1086                container_export(
1087                    &repo,
1088                    &rev,
1089                    &imgref,
1090                    labels?,
1091                    authfile,
1092                    copy_meta_keys,
1093                    copy_meta_opt_keys,
1094                    config,
1095                    cmd,
1096                    compression_fast,
1097                    contentmeta,
1098                )
1099                .await
1100            }
1101            ContainerOpts::Image(opts) => match opts {
1102                ContainerImageOpts::List { repo } => {
1103                    let repo = parse_repo(&repo)?;
1104                    for image in crate::container::store::list_images(&repo)? {
1105                        println!("{image}");
1106                    }
1107                    Ok(())
1108                }
1109                ContainerImageOpts::Pull {
1110                    repo,
1111                    imgref,
1112                    ostree_digestfile,
1113                    proxyopts,
1114                    quiet,
1115                    check,
1116                } => {
1117                    let repo = parse_repo(&repo)?;
1118                    container_store(&repo, &imgref, ostree_digestfile, proxyopts, quiet, check)
1119                        .await
1120                }
1121                ContainerImageOpts::Reexport {
1122                    repo,
1123                    src_imgref,
1124                    dest_imgref,
1125                    authfile,
1126                    compression_fast,
1127                } => {
1128                    let repo = &parse_repo(&repo)?;
1129                    let opts = ExportToOCIOpts {
1130                        authfile,
1131                        skip_compression: compression_fast,
1132                        ..Default::default()
1133                    };
1134                    let digest = ostree_container::store::export(
1135                        repo,
1136                        &src_imgref,
1137                        &dest_imgref,
1138                        Some(opts),
1139                    )
1140                    .await?;
1141                    println!("Exported: {digest}");
1142                    Ok(())
1143                }
1144                ContainerImageOpts::History { repo, imgref } => {
1145                    let repo = parse_repo(&repo)?;
1146                    container_history(&repo, &imgref).await
1147                }
1148                ContainerImageOpts::Metadata {
1149                    repo,
1150                    imgref,
1151                    config,
1152                } => {
1153                    let repo = parse_repo(&repo)?;
1154                    let image = crate::container::store::query_image(&repo, &imgref)?
1155                        .ok_or_else(|| anyhow::anyhow!("No such image"))?;
1156                    let stdout = std::io::stdout().lock();
1157                    let mut stdout = std::io::BufWriter::new(stdout);
1158                    if config {
1159                        image.configuration.to_canon_json_writer(&mut stdout)?;
1160                    } else {
1161                        image.manifest.to_canon_json_writer(&mut stdout)?;
1162                    }
1163                    stdout.flush()?;
1164                    Ok(())
1165                }
1166                ContainerImageOpts::ClearCachedUpdate { repo, imgref } => {
1167                    let repo = parse_repo(&repo)?;
1168                    crate::container::store::clear_cached_update(&repo, &imgref)?;
1169                    Ok(())
1170                }
1171                ContainerImageOpts::Remove {
1172                    repo,
1173                    imgrefs,
1174                    skip_gc,
1175                } => {
1176                    let nimgs = imgrefs.len();
1177                    let repo = parse_repo(&repo)?;
1178                    crate::container::store::remove_images(&repo, imgrefs.iter())?;
1179                    if !skip_gc {
1180                        let nlayers = crate::container::store::gc_image_layers(&repo)?;
1181                        println!("Removed images: {nimgs} layers: {nlayers}");
1182                    } else {
1183                        println!("Removed images: {nimgs}");
1184                    }
1185                    Ok(())
1186                }
1187                ContainerImageOpts::PruneLayers { repo } => {
1188                    let repo = parse_repo(&repo)?;
1189                    let nlayers = crate::container::store::gc_image_layers(&repo)?;
1190                    println!("Removed layers: {nlayers}");
1191                    Ok(())
1192                }
1193                ContainerImageOpts::PruneImages {
1194                    sysroot,
1195                    and_layers,
1196                    full,
1197                } => {
1198                    let sysroot = &ostree::Sysroot::new(Some(&gio::File::for_path(&sysroot)));
1199                    sysroot.load(gio::Cancellable::NONE)?;
1200                    let sysroot = &SysrootLock::new_from_sysroot(sysroot).await?;
1201                    if full {
1202                        let res = crate::container::deploy::prune(sysroot)?;
1203                        if res.is_empty() {
1204                            println!("No content was pruned.");
1205                        } else {
1206                            println!("Removed images: {}", res.n_images);
1207                            println!("Removed layers: {}", res.n_layers);
1208                            println!("Removed objects: {}", res.n_objects_pruned);
1209                            let objsize = glib::format_size(res.objsize);
1210                            println!("Freed: {objsize}");
1211                        }
1212                    } else {
1213                        let removed = crate::container::deploy::remove_undeployed_images(sysroot)?;
1214                        match removed.as_slice() {
1215                            [] => {
1216                                println!("No unreferenced images.");
1217                                return Ok(());
1218                            }
1219                            o => {
1220                                for imgref in o {
1221                                    println!("Removed: {imgref}");
1222                                }
1223                            }
1224                        }
1225                        if and_layers {
1226                            let nlayers =
1227                                crate::container::store::gc_image_layers(&sysroot.repo())?;
1228                            println!("Removed layers: {nlayers}");
1229                        }
1230                    }
1231                    Ok(())
1232                }
1233                ContainerImageOpts::Copy {
1234                    src_repo,
1235                    dest_repo,
1236                    imgref,
1237                } => {
1238                    let src_repo = parse_repo(&src_repo)?;
1239                    let dest_repo = parse_repo(&dest_repo)?;
1240                    let imgref = &imgref.imgref;
1241                    crate::container::store::copy(&src_repo, imgref, &dest_repo, imgref).await
1242                }
1243                ContainerImageOpts::ReplaceDetachedMetadata {
1244                    src,
1245                    dest,
1246                    contents,
1247                } => {
1248                    let contents = contents.map(std::fs::read).transpose()?;
1249                    let digest = crate::container::update_detached_metadata(
1250                        &src,
1251                        &dest,
1252                        contents.as_deref(),
1253                    )
1254                    .await?;
1255                    println!("Pushed: {digest}");
1256                    Ok(())
1257                }
1258                ContainerImageOpts::Deploy {
1259                    sysroot,
1260                    stateroot,
1261                    imgref,
1262                    image,
1263                    transport,
1264                    no_signature_verification: _,
1265                    enforce_container_sigpolicy,
1266                    ostree_remote,
1267                    target_imgref,
1268                    no_imgref,
1269                    karg,
1270                    proxyopts,
1271                    write_commitid_to,
1272                } => {
1273                    // As of recent releases, signature verification enforcement is
1274                    // off by default, and must be explicitly enabled.
1275                    let no_signature_verification = !enforce_container_sigpolicy;
1276                    let sysroot = &if let Some(sysroot) = sysroot {
1277                        ostree::Sysroot::new(Some(&gio::File::for_path(sysroot)))
1278                    } else {
1279                        ostree::Sysroot::new_default()
1280                    };
1281                    sysroot.load(gio::Cancellable::NONE)?;
1282                    let kargs = karg.as_deref();
1283                    let kargs = kargs.map(|v| {
1284                        let r: Vec<_> = v.iter().map(|s| s.as_str()).collect();
1285                        r
1286                    });
1287
1288                    // If the user specified a stateroot, we always use that.
1289                    let stateroot = if let Some(stateroot) = stateroot.as_deref() {
1290                        Cow::Borrowed(stateroot)
1291                    } else {
1292                        // Otherwise, if we're booted via ostree, use the booted.
1293                        // If that doesn't hold, then use `default`.
1294                        let booted_stateroot = sysroot
1295                            .booted_deployment()
1296                            .map(|d| Cow::Owned(d.osname().to_string()));
1297                        booted_stateroot.unwrap_or({
1298                            Cow::Borrowed(crate::container::deploy::STATEROOT_DEFAULT)
1299                        })
1300                    };
1301
1302                    let imgref = if let Some(image) = image {
1303                        let transport = transport.as_deref().unwrap_or("registry");
1304                        let transport = ostree_container::Transport::try_from(transport)?;
1305                        let imgref = ostree_container::ImageReference {
1306                            transport,
1307                            name: image,
1308                        };
1309                        let sigverify = if no_signature_verification {
1310                            ostree_container::SignatureSource::ContainerPolicyAllowInsecure
1311                        } else if let Some(remote) = ostree_remote.as_ref() {
1312                            ostree_container::SignatureSource::OstreeRemote(remote.to_string())
1313                        } else {
1314                            ostree_container::SignatureSource::ContainerPolicy
1315                        };
1316                        ostree_container::OstreeImageReference { sigverify, imgref }
1317                    } else {
1318                        // SAFETY: We use the clap required_unless_present flag, so this must be set
1319                        // because --image is not.
1320                        let imgref = imgref.expect("imgref option should be set");
1321                        imgref.as_str().try_into()?
1322                    };
1323
1324                    #[allow(clippy::needless_update)]
1325                    let options = crate::container::deploy::DeployOpts {
1326                        kargs: kargs.as_deref(),
1327                        target_imgref: target_imgref.as_ref(),
1328                        proxy_cfg: Some(proxyopts.into()),
1329                        no_imgref,
1330                        ..Default::default()
1331                    };
1332                    let state = crate::container::deploy::deploy(
1333                        sysroot,
1334                        &stateroot,
1335                        &imgref,
1336                        Some(options),
1337                    )
1338                    .await?;
1339                    if let Some(msg) = ostree_container::store::image_filtered_content_warning(
1340                        &state.filtered_files,
1341                    )? {
1342                        eprintln!("{msg}")
1343                    }
1344                    if let Some(p) = write_commitid_to {
1345                        std::fs::write(&p, state.merge_commit.as_bytes())
1346                            .with_context(|| format!("Failed to write commitid to {p}"))?;
1347                    }
1348                    Ok(())
1349                }
1350            },
1351            ContainerOpts::Compare {
1352                imgref_old,
1353                imgref_new,
1354            } => {
1355                let (manifest_old, _) = crate::container::fetch_manifest(&imgref_old).await?;
1356                let (manifest_new, _) = crate::container::fetch_manifest(&imgref_new).await?;
1357                let manifest_diff =
1358                    crate::container::ManifestDiff::new(&manifest_old, &manifest_new);
1359                manifest_diff.print();
1360                Ok(())
1361            }
1362        },
1363        Opt::ImaSign(ref opts) => ima_sign(opts),
1364        #[cfg(feature = "internal-testing-api")]
1365        Opt::InternalOnlyForTesting(ref opts) => testing(opts).await,
1366        #[cfg(feature = "docgen")]
1367        Opt::Man(manopts) => crate::docgen::generate_manpages(&manopts.directory),
1368        Opt::ProvisionalRepair(opts) => match opts {
1369            ProvisionalRepairOpts::AnalyzeInodes {
1370                repo,
1371                verbose,
1372                write_result_to,
1373            } => {
1374                let repo = parse_repo(&repo)?;
1375                let check_res = crate::repair::check_inode_collision(&repo, verbose)?;
1376                handle_serialize_to_file(write_result_to.as_deref(), &check_res)?;
1377                if check_res.collisions.is_empty() {
1378                    println!("OK: No colliding objects found.");
1379                } else {
1380                    eprintln!(
1381                        "warning: {} potentially colliding inodes found",
1382                        check_res.collisions.len()
1383                    );
1384                }
1385                Ok(())
1386            }
1387            ProvisionalRepairOpts::Repair {
1388                sysroot,
1389                verbose,
1390                dry_run,
1391                write_result_to,
1392            } => {
1393                container_remount_sysroot(&sysroot)?;
1394                let sysroot = &ostree::Sysroot::new(Some(&gio::File::for_path(&sysroot)));
1395                sysroot.load(gio::Cancellable::NONE)?;
1396                let sysroot = &SysrootLock::new_from_sysroot(sysroot).await?;
1397                let result = crate::repair::analyze_for_repair(sysroot, verbose)?;
1398                handle_serialize_to_file(write_result_to.as_deref(), &result)?;
1399                if dry_run {
1400                    result.check()
1401                } else {
1402                    result.repair(sysroot)
1403                }
1404            }
1405        },
1406    }
1407}