1use 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
39pub fn parse_imgref(s: &str) -> Result<OstreeImageReference> {
41 OstreeImageReference::try_from(s)
42}
43
44pub fn parse_base_imgref(s: &str) -> Result<ImageReference> {
46 ImageReference::try_from(s)
47}
48
49pub 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#[derive(Debug, Parser)]
59pub(crate) struct ImportOpts {
60 #[clap(long, value_parser)]
62 repo: Utf8PathBuf,
63
64 path: Option<String>,
66}
67
68#[derive(Debug, Parser)]
70pub(crate) struct ExportOpts {
71 #[clap(long, value_parser)]
73 repo: Utf8PathBuf,
74
75 #[clap(long, hide(true))]
77 format_version: u32,
78
79 rev: String,
81}
82
83#[derive(Debug, Subcommand)]
85pub(crate) enum TarOpts {
86 Import(ImportOpts),
88
89 Export(ExportOpts),
91}
92
93#[derive(Debug, Subcommand)]
95pub(crate) enum ContainerOpts {
96 #[clap(alias = "import")]
97 Unencapsulate {
99 #[clap(long, value_parser)]
101 repo: Utf8PathBuf,
102
103 #[clap(flatten)]
104 proxyopts: ContainerProxyOpts,
105
106 #[clap(value_parser = parse_imgref)]
108 imgref: OstreeImageReference,
109
110 #[clap(long)]
112 write_ref: Option<String>,
113
114 #[clap(long)]
116 quiet: bool,
117 },
118
119 Info {
121 #[clap(value_parser = parse_imgref)]
123 imgref: OstreeImageReference,
124 },
125
126 #[clap(alias = "export")]
134 Encapsulate {
135 #[clap(long, value_parser)]
137 repo: Utf8PathBuf,
138
139 rev: String,
141
142 #[clap(value_parser = parse_base_imgref)]
144 imgref: ImageReference,
145
146 #[clap(name = "label", long, short)]
148 labels: Vec<String>,
149
150 #[clap(long)]
151 authfile: Option<PathBuf>,
153
154 #[clap(long)]
157 config: Option<Utf8PathBuf>,
158
159 #[clap(name = "copymeta", long)]
161 copy_meta_keys: Vec<String>,
162
163 #[clap(name = "copymeta-opt", long)]
165 copy_meta_opt_keys: Vec<String>,
166
167 #[clap(long)]
169 cmd: Option<Vec<String>>,
170
171 #[clap(long)]
173 compression_fast: bool,
174
175 #[clap(long)]
177 contentmeta: Option<Utf8PathBuf>,
178 },
179
180 Commit,
183
184 #[clap(subcommand)]
186 Image(ContainerImageOpts),
187
188 Compare {
190 #[clap(value_parser = parse_imgref)]
192 imgref_old: OstreeImageReference,
193
194 #[clap(value_parser = parse_imgref)]
196 imgref_new: OstreeImageReference,
197 },
198}
199
200#[derive(Debug, Parser)]
202pub(crate) struct ContainerProxyOpts {
203 #[clap(long)]
204 auth_anonymous: bool,
206
207 #[clap(long)]
208 authfile: Option<PathBuf>,
210
211 #[clap(long)]
212 cert_dir: Option<PathBuf>,
215
216 #[clap(long)]
217 insecure_skip_tls_verification: bool,
219}
220
221#[derive(Debug, Subcommand)]
223pub(crate) enum ContainerImageOpts {
224 List {
226 #[clap(long, value_parser)]
228 repo: Utf8PathBuf,
229 },
230
231 Pull {
233 #[clap(value_parser)]
235 repo: Utf8PathBuf,
236
237 #[clap(value_parser = parse_imgref)]
239 imgref: OstreeImageReference,
240
241 #[clap(long)]
243 ostree_digestfile: Option<Utf8PathBuf>,
244
245 #[clap(flatten)]
246 proxyopts: ContainerProxyOpts,
247
248 #[clap(long)]
250 quiet: bool,
251
252 #[clap(long)]
256 check: Option<Utf8PathBuf>,
257 },
258
259 History {
261 #[clap(long, value_parser)]
263 repo: Utf8PathBuf,
264
265 #[clap(value_parser = parse_base_imgref)]
267 imgref: ImageReference,
268 },
269
270 Metadata {
272 #[clap(long, value_parser)]
274 repo: Utf8PathBuf,
275
276 #[clap(value_parser = parse_base_imgref)]
278 imgref: ImageReference,
279
280 #[clap(long)]
282 config: bool,
283 },
284
285 ClearCachedUpdate {
287 #[clap(long, value_parser)]
289 repo: Utf8PathBuf,
290
291 #[clap(value_parser = parse_base_imgref)]
293 imgref: ImageReference,
294 },
295
296 Copy {
298 #[clap(long, value_parser)]
300 src_repo: Utf8PathBuf,
301
302 #[clap(long, value_parser)]
304 dest_repo: Utf8PathBuf,
305
306 #[clap(value_parser = parse_imgref)]
308 imgref: OstreeImageReference,
309 },
310
311 Reexport {
316 #[clap(long, value_parser)]
318 repo: Utf8PathBuf,
319
320 #[clap(value_parser = parse_base_imgref)]
322 src_imgref: ImageReference,
323
324 #[clap(value_parser = parse_base_imgref)]
326 dest_imgref: ImageReference,
327
328 #[clap(long)]
329 authfile: Option<PathBuf>,
331
332 #[clap(long)]
334 compression_fast: bool,
335 },
336
337 ReplaceDetachedMetadata {
339 #[clap(long)]
341 #[clap(value_parser = parse_base_imgref)]
342 src: ImageReference,
343
344 #[clap(long)]
346 #[clap(value_parser = parse_base_imgref)]
347 dest: ImageReference,
348
349 contents: Option<Utf8PathBuf>,
352 },
353
354 Remove {
356 #[clap(long, value_parser)]
358 repo: Utf8PathBuf,
359
360 #[clap(value_parser = parse_base_imgref)]
362 imgrefs: Vec<ImageReference>,
363
364 #[clap(long)]
366 skip_gc: bool,
367 },
368
369 PruneLayers {
371 #[clap(long, value_parser)]
373 repo: Utf8PathBuf,
374 },
375
376 PruneImages {
378 #[clap(long)]
380 sysroot: Utf8PathBuf,
381
382 #[clap(long)]
383 and_layers: bool,
385
386 #[clap(long, conflicts_with = "and_layers")]
387 full: bool,
389 },
390
391 Deploy {
393 #[clap(long)]
395 sysroot: Option<String>,
396
397 #[clap(long)]
401 stateroot: Option<String>,
402
403 #[clap(long, required_unless_present = "image")]
407 imgref: Option<String>,
408
409 #[clap(long, required_unless_present = "imgref")]
412 image: Option<String>,
413
414 #[clap(long)]
416 transport: Option<String>,
417
418 #[clap(long, conflicts_with = "enforce_container_sigpolicy")]
423 no_signature_verification: bool,
424
425 #[clap(long)]
427 enforce_container_sigpolicy: bool,
428
429 #[clap(long)]
431 ostree_remote: Option<String>,
432
433 #[clap(flatten)]
434 proxyopts: ContainerProxyOpts,
435
436 #[clap(long)]
441 #[clap(value_parser = parse_imgref)]
442 target_imgref: Option<OstreeImageReference>,
443
444 #[clap(long)]
449 no_imgref: bool,
450
451 #[clap(long)]
452 karg: Option<Vec<String>>,
454
455 #[clap(long)]
457 write_commitid_to: Option<Utf8PathBuf>,
458 },
459}
460
461#[derive(Debug, Parser)]
463pub(crate) enum ProvisionalRepairOpts {
464 AnalyzeInodes {
465 #[clap(long, value_parser)]
467 repo: Utf8PathBuf,
468
469 #[clap(long)]
471 verbose: bool,
472
473 #[clap(long)]
475 write_result_to: Option<Utf8PathBuf>,
476 },
477
478 Repair {
479 #[clap(long, value_parser)]
481 sysroot: Utf8PathBuf,
482
483 #[clap(long)]
485 dry_run: bool,
486
487 #[clap(long)]
489 write_result_to: Option<Utf8PathBuf>,
490
491 #[clap(long)]
493 verbose: bool,
494 },
495}
496
497#[derive(Debug, Parser)]
499pub(crate) struct ImaSignOpts {
500 #[clap(long, value_parser)]
502 repo: Utf8PathBuf,
503
504 src_rev: String,
506 target_ref: String,
508
509 algorithm: String,
511 key: Utf8PathBuf,
513
514 #[clap(long)]
515 overwrite: bool,
517}
518
519#[derive(Debug, Subcommand)]
521pub(crate) enum TestingOpts {
522 DetectEnv,
524 CreateFixture,
526 Run,
528 RunIMA,
530 FilterTar,
531}
532
533#[derive(Debug, Parser)]
535pub(crate) struct ManOpts {
536 #[clap(long)]
537 directory: Utf8PathBuf,
539}
540
541#[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 #[clap(subcommand)]
549 Tar(TarOpts),
550 #[clap(subcommand)]
552 Container(ContainerOpts),
553 ImaSign(ImaSignOpts),
555 #[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
578async 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
592fn 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
603pub 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 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
626pub 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 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 break
653 };
654 },
655 r = layer_bytes.changed() => {
656 if r.is_err() {
657 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
671pub 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
679pub async fn print_deprecated_warning(msg: &str) {
681 eprintln!("warning: {msg}");
682 tokio::time::sleep(std::time::Duration::from_secs(3)).await
683}
684
685async 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 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#[derive(Debug, Default, Serialize, Deserialize)]
734pub struct RawMeta {
735 pub version: u32,
737 pub created: Option<String>,
740 pub labels: Option<BTreeMap<String, String>>,
743 pub layers: IndexMap<String, String>,
747 pub mapping: IndexMap<String, String>,
751 pub ordered: Option<bool>,
757}
758
759#[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 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 labels.extend(raw.labels.into_iter().flatten());
827 }
828
829 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, 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
858async fn container_info(imgref: &OstreeImageReference) -> Result<()> {
860 let (_, digest) = crate::container::fetch_manifest(imgref).await?;
861 println!("{imgref} digest: {digest}");
862 Ok(())
863}
864
865async 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 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
934async 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 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
964fn 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#[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
1036pub 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 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 let stateroot = if let Some(stateroot) = stateroot.as_deref() {
1290 Cow::Borrowed(stateroot)
1291 } else {
1292 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 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}