1use anyhow::{Context, Result};
9use camino::{Utf8Path, Utf8PathBuf};
10use cap_std::fs::Dir;
11use cap_std_ext::cap_std;
12use cap_std_ext::prelude::CapStdExtDirExt;
13use clap::{Parser, Subcommand};
14use fn_error_context::context;
15use indexmap::IndexMap;
16use io_lifetimes::AsFd;
17use ostree::{gio, glib};
18use std::borrow::Cow;
19use std::collections::BTreeMap;
20use std::ffi::OsString;
21use std::fs::File;
22use std::io::{BufReader, BufWriter, Write};
23use std::num::NonZeroU32;
24use std::path::PathBuf;
25use std::process::Command;
26use tokio::sync::mpsc::Receiver;
27
28use crate::chunking::{ObjectMetaSized, ObjectSourceMetaSized};
29use crate::commit::container_commit;
30use crate::container::store::{ExportToOCIOpts, ImportProgress, LayerProgress, PreparedImport};
31use crate::container::{self as ostree_container, ManifestDiff};
32use crate::container::{Config, ImageReference, OstreeImageReference};
33use crate::objectsource::ObjectSourceMeta;
34use crate::sysroot::SysrootLock;
35use ostree_container::store::{ImageImporter, PrepareResult};
36use serde::{Deserialize, Serialize};
37
38pub fn parse_imgref(s: &str) -> Result<OstreeImageReference> {
40 OstreeImageReference::try_from(s)
41}
42
43pub fn parse_base_imgref(s: &str) -> Result<ImageReference> {
45 ImageReference::try_from(s)
46}
47
48pub fn parse_repo(s: &Utf8Path) -> Result<ostree::Repo> {
50 let repofd = cap_std::fs::Dir::open_ambient_dir(s, cap_std::ambient_authority())
51 .with_context(|| format!("Opening directory at '{s}'"))?;
52 ostree::Repo::open_at_dir(repofd.as_fd(), ".")
53 .with_context(|| format!("Opening ostree repository at '{s}'"))
54}
55
56#[derive(Debug, Parser)]
58pub(crate) struct ImportOpts {
59 #[clap(long, value_parser)]
61 repo: Utf8PathBuf,
62
63 path: Option<String>,
65}
66
67#[derive(Debug, Parser)]
69pub(crate) struct ExportOpts {
70 #[clap(long, value_parser)]
72 repo: Utf8PathBuf,
73
74 #[clap(long, hide(true))]
76 format_version: u32,
77
78 rev: String,
80}
81
82#[derive(Debug, Subcommand)]
84pub(crate) enum TarOpts {
85 Import(ImportOpts),
87
88 Export(ExportOpts),
90}
91
92#[derive(Debug, Subcommand)]
94pub(crate) enum ContainerOpts {
95 #[clap(alias = "import")]
96 Unencapsulate {
98 #[clap(long, value_parser)]
100 repo: Utf8PathBuf,
101
102 #[clap(flatten)]
103 proxyopts: ContainerProxyOpts,
104
105 #[clap(value_parser = parse_imgref)]
107 imgref: OstreeImageReference,
108
109 #[clap(long)]
111 write_ref: Option<String>,
112
113 #[clap(long)]
115 quiet: bool,
116 },
117
118 Info {
120 #[clap(value_parser = parse_imgref)]
122 imgref: OstreeImageReference,
123 },
124
125 #[clap(alias = "export")]
133 Encapsulate {
134 #[clap(long, value_parser)]
136 repo: Utf8PathBuf,
137
138 rev: String,
140
141 #[clap(value_parser = parse_base_imgref)]
143 imgref: ImageReference,
144
145 #[clap(name = "label", long, short)]
147 labels: Vec<String>,
148
149 #[clap(long)]
150 authfile: Option<PathBuf>,
152
153 #[clap(long)]
156 config: Option<Utf8PathBuf>,
157
158 #[clap(name = "copymeta", long)]
160 copy_meta_keys: Vec<String>,
161
162 #[clap(name = "copymeta-opt", long)]
164 copy_meta_opt_keys: Vec<String>,
165
166 #[clap(long)]
168 cmd: Option<Vec<String>>,
169
170 #[clap(long)]
172 compression_fast: bool,
173
174 #[clap(long)]
176 contentmeta: Option<Utf8PathBuf>,
177 },
178
179 Commit,
182
183 #[clap(subcommand)]
185 Image(ContainerImageOpts),
186
187 Compare {
189 #[clap(value_parser = parse_imgref)]
191 imgref_old: OstreeImageReference,
192
193 #[clap(value_parser = parse_imgref)]
195 imgref_new: OstreeImageReference,
196 },
197}
198
199#[derive(Debug, Parser)]
201pub(crate) struct ContainerProxyOpts {
202 #[clap(long)]
203 auth_anonymous: bool,
205
206 #[clap(long)]
207 authfile: Option<PathBuf>,
209
210 #[clap(long)]
211 cert_dir: Option<PathBuf>,
214
215 #[clap(long)]
216 insecure_skip_tls_verification: bool,
218}
219
220#[derive(Debug, Subcommand)]
222pub(crate) enum ContainerImageOpts {
223 List {
225 #[clap(long, value_parser)]
227 repo: Utf8PathBuf,
228 },
229
230 Pull {
232 #[clap(value_parser)]
234 repo: Utf8PathBuf,
235
236 #[clap(value_parser = parse_imgref)]
238 imgref: OstreeImageReference,
239
240 #[clap(flatten)]
241 proxyopts: ContainerProxyOpts,
242
243 #[clap(long)]
245 quiet: bool,
246
247 #[clap(long)]
251 check: Option<Utf8PathBuf>,
252 },
253
254 History {
256 #[clap(long, value_parser)]
258 repo: Utf8PathBuf,
259
260 #[clap(value_parser = parse_base_imgref)]
262 imgref: ImageReference,
263 },
264
265 Metadata {
267 #[clap(long, value_parser)]
269 repo: Utf8PathBuf,
270
271 #[clap(value_parser = parse_base_imgref)]
273 imgref: ImageReference,
274
275 #[clap(long)]
277 config: bool,
278 },
279
280 Copy {
282 #[clap(long, value_parser)]
284 src_repo: Utf8PathBuf,
285
286 #[clap(long, value_parser)]
288 dest_repo: Utf8PathBuf,
289
290 #[clap(value_parser = parse_imgref)]
292 imgref: OstreeImageReference,
293 },
294
295 Reexport {
300 #[clap(long, value_parser)]
302 repo: Utf8PathBuf,
303
304 #[clap(value_parser = parse_base_imgref)]
306 src_imgref: ImageReference,
307
308 #[clap(value_parser = parse_base_imgref)]
310 dest_imgref: ImageReference,
311
312 #[clap(long)]
313 authfile: Option<PathBuf>,
315
316 #[clap(long)]
318 compression_fast: bool,
319 },
320
321 ReplaceDetachedMetadata {
323 #[clap(long)]
325 #[clap(value_parser = parse_base_imgref)]
326 src: ImageReference,
327
328 #[clap(long)]
330 #[clap(value_parser = parse_base_imgref)]
331 dest: ImageReference,
332
333 contents: Option<Utf8PathBuf>,
336 },
337
338 Remove {
340 #[clap(long, value_parser)]
342 repo: Utf8PathBuf,
343
344 #[clap(value_parser = parse_base_imgref)]
346 imgrefs: Vec<ImageReference>,
347
348 #[clap(long)]
350 skip_gc: bool,
351 },
352
353 PruneLayers {
355 #[clap(long, value_parser)]
357 repo: Utf8PathBuf,
358 },
359
360 PruneImages {
362 #[clap(long)]
364 sysroot: Utf8PathBuf,
365
366 #[clap(long)]
367 and_layers: bool,
369
370 #[clap(long, conflicts_with = "and_layers")]
371 full: bool,
373 },
374
375 Deploy {
377 #[clap(long)]
379 sysroot: Option<String>,
380
381 #[clap(long)]
385 stateroot: Option<String>,
386
387 #[clap(long, required_unless_present = "image")]
391 imgref: Option<String>,
392
393 #[clap(long, required_unless_present = "imgref")]
396 image: Option<String>,
397
398 #[clap(long)]
400 transport: Option<String>,
401
402 #[clap(long, conflicts_with = "enforce_container_sigpolicy")]
407 no_signature_verification: bool,
408
409 #[clap(long)]
411 enforce_container_sigpolicy: bool,
412
413 #[clap(long)]
415 ostree_remote: Option<String>,
416
417 #[clap(flatten)]
418 proxyopts: ContainerProxyOpts,
419
420 #[clap(long)]
425 #[clap(value_parser = parse_imgref)]
426 target_imgref: Option<OstreeImageReference>,
427
428 #[clap(long)]
433 no_imgref: bool,
434
435 #[clap(long)]
436 karg: Option<Vec<String>>,
438
439 #[clap(long)]
441 write_commitid_to: Option<Utf8PathBuf>,
442 },
443}
444
445#[derive(Debug, Parser)]
447pub(crate) enum ProvisionalRepairOpts {
448 AnalyzeInodes {
449 #[clap(long, value_parser)]
451 repo: Utf8PathBuf,
452
453 #[clap(long)]
455 verbose: bool,
456
457 #[clap(long)]
459 write_result_to: Option<Utf8PathBuf>,
460 },
461
462 Repair {
463 #[clap(long, value_parser)]
465 sysroot: Utf8PathBuf,
466
467 #[clap(long)]
469 dry_run: bool,
470
471 #[clap(long)]
473 write_result_to: Option<Utf8PathBuf>,
474
475 #[clap(long)]
477 verbose: bool,
478 },
479}
480
481#[derive(Debug, Parser)]
483pub(crate) struct ImaSignOpts {
484 #[clap(long, value_parser)]
486 repo: Utf8PathBuf,
487
488 src_rev: String,
490 target_ref: String,
492
493 algorithm: String,
495 key: Utf8PathBuf,
497
498 #[clap(long)]
499 overwrite: bool,
501}
502
503#[derive(Debug, Subcommand)]
505pub(crate) enum TestingOpts {
506 DetectEnv,
508 CreateFixture,
510 Run,
512 RunIMA,
514 FilterTar,
515}
516
517#[derive(Debug, Parser)]
519pub(crate) struct ManOpts {
520 #[clap(long)]
521 directory: Utf8PathBuf,
523}
524
525#[derive(Debug, Parser)]
527#[clap(name = "ostree-ext")]
528#[clap(rename_all = "kebab-case")]
529#[allow(clippy::large_enum_variant)]
530pub(crate) enum Opt {
531 #[clap(subcommand)]
533 Tar(TarOpts),
534 #[clap(subcommand)]
536 Container(ContainerOpts),
537 ImaSign(ImaSignOpts),
539 #[clap(hide(true), subcommand)]
541 #[cfg(feature = "internal-testing-api")]
542 InternalOnlyForTesting(TestingOpts),
543 #[clap(hide(true))]
544 #[cfg(feature = "docgen")]
545 Man(ManOpts),
546 #[clap(hide = true, subcommand)]
547 ProvisionalRepair(ProvisionalRepairOpts),
548}
549
550#[allow(clippy::from_over_into)]
551impl Into<ostree_container::store::ImageProxyConfig> for ContainerProxyOpts {
552 fn into(self) -> ostree_container::store::ImageProxyConfig {
553 ostree_container::store::ImageProxyConfig {
554 auth_anonymous: self.auth_anonymous,
555 authfile: self.authfile,
556 certificate_directory: self.cert_dir,
557 insecure_skip_tls_verification: Some(self.insecure_skip_tls_verification),
558 ..Default::default()
559 }
560 }
561}
562
563async fn tar_import(opts: &ImportOpts) -> Result<()> {
565 let repo = parse_repo(&opts.repo)?;
566 let imported = if let Some(path) = opts.path.as_ref() {
567 let instream = tokio::fs::File::open(path).await?;
568 crate::tar::import_tar(&repo, instream, None).await?
569 } else {
570 let stdin = tokio::io::stdin();
571 crate::tar::import_tar(&repo, stdin, None).await?
572 };
573 println!("Imported: {}", imported);
574 Ok(())
575}
576
577fn tar_export(opts: &ExportOpts) -> Result<()> {
579 let repo = parse_repo(&opts.repo)?;
580 #[allow(clippy::needless_update)]
581 let subopts = crate::tar::ExportOptions {
582 ..Default::default()
583 };
584 crate::tar::export_commit(&repo, opts.rev.as_str(), std::io::stdout(), Some(subopts))?;
585 Ok(())
586}
587
588pub fn layer_progress_format(p: &ImportProgress) -> String {
590 let (starting, s, layer) = match p {
591 ImportProgress::OstreeChunkStarted(v) => (true, "ostree chunk", v),
592 ImportProgress::OstreeChunkCompleted(v) => (false, "ostree chunk", v),
593 ImportProgress::DerivedLayerStarted(v) => (true, "layer", v),
594 ImportProgress::DerivedLayerCompleted(v) => (false, "layer", v),
595 };
596 let short_digest = layer
598 .digest()
599 .digest()
600 .chars()
601 .take(12 + 7)
602 .collect::<String>();
603 if starting {
604 let size = glib::format_size(layer.size());
605 format!("Fetching {s} {short_digest} ({size})")
606 } else {
607 format!("Fetched {s} {short_digest}")
608 }
609}
610
611pub async fn handle_layer_progress_print(
613 mut layers: Receiver<ImportProgress>,
614 mut layer_bytes: tokio::sync::watch::Receiver<Option<LayerProgress>>,
615) {
616 let style = indicatif::ProgressStyle::default_bar();
617 let pb = indicatif::ProgressBar::new(100);
618 pb.set_style(
619 style
620 .template("{prefix} {bytes} [{bar:20}] ({eta}) {msg}")
621 .unwrap(),
622 );
623 loop {
624 tokio::select! {
625 biased;
627 layer = layers.recv() => {
628 if let Some(l) = layer {
629 if l.is_starting() {
630 pb.set_position(0);
631 } else {
632 pb.finish();
633 }
634 pb.set_message(layer_progress_format(&l));
635 } else {
636 break
638 };
639 },
640 r = layer_bytes.changed() => {
641 if r.is_err() {
642 break
644 }
645 let bytes = layer_bytes.borrow();
646 if let Some(bytes) = &*bytes {
647 pb.set_length(bytes.total);
648 pb.set_position(bytes.fetched);
649 }
650 }
651
652 }
653 }
654}
655
656pub fn print_layer_status(prep: &PreparedImport) {
658 if let Some(status) = prep.format_layer_status() {
659 println!("{status}");
660 }
661}
662
663pub async fn print_deprecated_warning(msg: &str) {
665 eprintln!("warning: {msg}");
666 tokio::time::sleep(std::time::Duration::from_secs(3)).await
667}
668
669async fn container_import(
671 repo: &ostree::Repo,
672 imgref: &OstreeImageReference,
673 proxyopts: ContainerProxyOpts,
674 write_ref: Option<&str>,
675 quiet: bool,
676) -> Result<()> {
677 let target = indicatif::ProgressDrawTarget::stdout();
678 let style = indicatif::ProgressStyle::default_bar();
679 let pb = (!quiet).then(|| {
680 let pb = indicatif::ProgressBar::new_spinner();
681 pb.set_draw_target(target);
682 pb.set_style(style.template("{spinner} {prefix} {msg}").unwrap());
683 pb.enable_steady_tick(std::time::Duration::from_millis(200));
684 pb.set_message("Downloading...");
685 pb
686 });
687 let importer = ImageImporter::new(repo, imgref, proxyopts.into()).await?;
688 let import = importer.unencapsulate().await;
689 if let Some(pb) = pb.as_ref() {
691 pb.finish();
692 }
693 let import = import?;
694 if let Some(warning) = import.deprecated_warning.as_deref() {
695 print_deprecated_warning(warning).await;
696 }
697 if let Some(write_ref) = write_ref {
698 repo.set_ref_immediate(
699 None,
700 write_ref,
701 Some(import.ostree_commit.as_str()),
702 gio::Cancellable::NONE,
703 )?;
704 println!(
705 "Imported: {} => {}",
706 write_ref,
707 import.ostree_commit.as_str()
708 );
709 } else {
710 println!("Imported: {}", import.ostree_commit);
711 }
712
713 Ok(())
714}
715
716#[derive(Debug, Default, Serialize, Deserialize)]
718pub struct RawMeta {
719 pub version: u32,
721 pub created: Option<String>,
724 pub labels: Option<BTreeMap<String, String>>,
727 pub layers: IndexMap<String, String>,
731 pub mapping: IndexMap<String, String>,
735 pub ordered: Option<bool>,
741}
742
743#[allow(clippy::too_many_arguments)]
745async fn container_export(
746 repo: &ostree::Repo,
747 rev: &str,
748 imgref: &ImageReference,
749 labels: BTreeMap<String, String>,
750 authfile: Option<PathBuf>,
751 copy_meta_keys: Vec<String>,
752 copy_meta_opt_keys: Vec<String>,
753 container_config: Option<Utf8PathBuf>,
754 cmd: Option<Vec<String>>,
755 compression_fast: bool,
756 contentmeta: Option<Utf8PathBuf>,
757) -> Result<()> {
758 let container_config = if let Some(container_config) = container_config {
759 serde_json::from_reader(File::open(container_config).map(BufReader::new)?)?
760 } else {
761 None
762 };
763
764 let mut contentmeta_data = None;
765 let mut created = None;
766 let mut labels = labels.clone();
767 if let Some(contentmeta) = contentmeta {
768 let buf = File::open(contentmeta).map(BufReader::new);
769 let raw: RawMeta = serde_json::from_reader(buf?)?;
770
771 let supported_version = 1;
773 if raw.version != supported_version {
774 return Err(anyhow::anyhow!(
775 "Unsupported metadata version: {}. Currently supported: {}",
776 raw.version,
777 supported_version
778 ));
779 }
780 if let Some(ordered) = raw.ordered {
781 if ordered {
782 return Err(anyhow::anyhow!("Ordered mapping not currently supported."));
783 }
784 }
785
786 created = raw.created;
787 contentmeta_data = Some(ObjectMetaSized {
788 map: raw
789 .mapping
790 .into_iter()
791 .map(|(k, v)| (k, v.into()))
792 .collect(),
793 sizes: raw
794 .layers
795 .into_iter()
796 .map(|(k, v)| ObjectSourceMetaSized {
797 meta: ObjectSourceMeta {
798 identifier: k.clone().into(),
799 name: v.into(),
800 srcid: k.clone().into(),
801 change_frequency: if k == "unpackaged" { u32::MAX } else { 1 },
802 change_time_offset: 1,
803 },
804 size: 1,
805 })
806 .collect(),
807 });
808
809 labels.extend(raw.labels.into_iter().flatten());
811 }
812
813 let max_layers = if let Some(contentmeta_data) = &contentmeta_data {
816 NonZeroU32::new((contentmeta_data.sizes.len() + 1).try_into().unwrap())
817 } else {
818 None
819 };
820
821 let config = Config {
822 labels: Some(labels),
823 cmd,
824 };
825
826 let opts = crate::container::ExportOpts {
827 copy_meta_keys,
828 copy_meta_opt_keys,
829 container_config,
830 authfile,
831 skip_compression: compression_fast, contentmeta: contentmeta_data.as_ref(),
833 max_layers,
834 created,
835 ..Default::default()
836 };
837 let pushed = crate::container::encapsulate(repo, rev, &config, Some(opts), imgref).await?;
838 println!("{}", pushed);
839 Ok(())
840}
841
842async fn container_info(imgref: &OstreeImageReference) -> Result<()> {
844 let (_, digest) = crate::container::fetch_manifest(imgref).await?;
845 println!("{} digest: {}", imgref, digest);
846 Ok(())
847}
848
849async fn container_store(
851 repo: &ostree::Repo,
852 imgref: &OstreeImageReference,
853 proxyopts: ContainerProxyOpts,
854 quiet: bool,
855 check: Option<Utf8PathBuf>,
856) -> Result<()> {
857 let mut imp = ImageImporter::new(repo, imgref, proxyopts.into()).await?;
858 let prep = match imp.prepare().await? {
859 PrepareResult::AlreadyPresent(c) => {
860 println!("No changes in {} => {}", imgref, c.merge_commit);
861 return Ok(());
862 }
863 PrepareResult::Ready(r) => r,
864 };
865 if let Some(warning) = prep.deprecated_warning() {
866 print_deprecated_warning(warning).await;
867 }
868 if let Some(check) = check.as_deref() {
869 let rootfs = Dir::open_ambient_dir("/", cap_std::ambient_authority())?;
870 rootfs.atomic_replace_with(check.as_str().trim_start_matches('/'), |w| {
871 serde_json::to_writer(w, &prep.manifest).context("Serializing manifest")
872 })?;
873 return Ok(());
875 }
876 if let Some(previous_state) = prep.previous_state.as_ref() {
877 let diff = ManifestDiff::new(&previous_state.manifest, &prep.manifest);
878 diff.print();
879 }
880 print_layer_status(&prep);
881 let printer = (!quiet).then(|| {
882 let layer_progress = imp.request_progress();
883 let layer_byte_progress = imp.request_layer_progress();
884 tokio::task::spawn(async move {
885 handle_layer_progress_print(layer_progress, layer_byte_progress).await
886 })
887 });
888 let import = imp.import(prep).await;
889 if let Some(printer) = printer {
890 let _ = printer.await;
891 }
892 let import = import?;
893 if let Some(msg) =
894 ostree_container::store::image_filtered_content_warning(repo, &imgref.imgref)?
895 {
896 eprintln!("{msg}")
897 }
898 println!("Wrote: {} => {}", imgref, import.merge_commit);
899 Ok(())
900}
901
902fn print_column(s: &str, clen: u16, remaining: &mut terminal_size::Width) {
903 let l: u16 = s.len().try_into().unwrap();
904 let l = l.min(remaining.0);
905 print!("{}", &s[0..l as usize]);
906 if clen > 0 {
907 let pad = clen.saturating_sub(l) + 2;
909 for _ in 0..pad {
910 print!(" ");
911 }
912 remaining.0 = remaining.0.checked_sub(l + pad).unwrap();
913 }
914}
915
916async fn container_history(repo: &ostree::Repo, imgref: &ImageReference) -> Result<()> {
918 let img = crate::container::store::query_image(repo, imgref)?
919 .ok_or_else(|| anyhow::anyhow!("No such image: {}", imgref))?;
920 let columns = [("ID", 20u16), ("SIZE", 10), ("CREATED BY", 0)];
921 let width = terminal_size::terminal_size()
922 .map(|x| x.0)
923 .unwrap_or(terminal_size::Width(80));
924 {
925 let mut remaining = width;
926 for (name, width) in columns.iter() {
927 print_column(name, *width, &mut remaining);
928 }
929 println!();
930 }
931
932 let mut history = img.configuration.history().iter();
933 let layers = img.manifest.layers().iter();
934 for layer in layers {
935 let histent = history.next();
936 let created_by = histent
937 .and_then(|s| s.created_by().as_deref())
938 .unwrap_or("");
939
940 let mut remaining = width;
941
942 let digest = layer.digest().digest();
943 assert!(digest.is_ascii());
945 let digest_max = columns[0].1;
946 let digest = &digest[0..digest_max as usize];
947 print_column(digest, digest_max, &mut remaining);
948 let size = glib::format_size(layer.size());
949 print_column(size.as_str(), columns[1].1, &mut remaining);
950 print_column(created_by, columns[2].1, &mut remaining);
951 println!();
952 }
953 Ok(())
954}
955
956fn ima_sign(cmdopts: &ImaSignOpts) -> Result<()> {
958 let cancellable = gio::Cancellable::NONE;
959 let signopts = crate::ima::ImaOpts {
960 algorithm: cmdopts.algorithm.clone(),
961 key: cmdopts.key.clone(),
962 overwrite: cmdopts.overwrite,
963 };
964 let repo = parse_repo(&cmdopts.repo)?;
965 let tx = repo.auto_transaction(cancellable)?;
966 let signed_commit = crate::ima::ima_sign(&repo, cmdopts.src_rev.as_str(), &signopts)?;
967 repo.transaction_set_ref(
968 None,
969 cmdopts.target_ref.as_str(),
970 Some(signed_commit.as_str()),
971 );
972 let _stats = tx.commit(cancellable)?;
973 println!("{} => {}", cmdopts.target_ref, signed_commit);
974 Ok(())
975}
976
977#[cfg(feature = "internal-testing-api")]
978async fn testing(opts: &TestingOpts) -> Result<()> {
979 match opts {
980 TestingOpts::DetectEnv => {
981 println!("{}", crate::integrationtest::detectenv()?);
982 Ok(())
983 }
984 TestingOpts::CreateFixture => crate::integrationtest::create_fixture().await,
985 TestingOpts::Run => crate::integrationtest::run_tests(),
986 TestingOpts::RunIMA => crate::integrationtest::test_ima(),
987 TestingOpts::FilterTar => {
988 let tmpdir = cap_std_ext::cap_tempfile::TempDir::new(cap_std::ambient_authority())?;
989 crate::tar::filter_tar(
990 std::io::stdin(),
991 std::io::stdout(),
992 &Default::default(),
993 &tmpdir,
994 )
995 .map(|_| {})
996 }
997 }
998}
999
1000#[context("Remounting sysroot writable")]
1002fn container_remount_sysroot(sysroot: &Utf8Path) -> Result<()> {
1003 if !Utf8Path::new("/run/.containerenv").exists() {
1004 return Ok(());
1005 }
1006 println!("Running in container, assuming we can remount {sysroot} writable");
1007 let st = Command::new("mount")
1008 .args(["-o", "remount,rw", sysroot.as_str()])
1009 .status()?;
1010 if !st.success() {
1011 anyhow::bail!("Failed to remount {sysroot}: {st:?}");
1012 }
1013 Ok(())
1014}
1015
1016#[context("Serializing to output file")]
1017fn handle_serialize_to_file<T: serde::Serialize>(path: Option<&Utf8Path>, obj: T) -> Result<()> {
1018 if let Some(path) = path {
1019 let mut out = std::fs::File::create(path)
1020 .map(BufWriter::new)
1021 .with_context(|| anyhow::anyhow!("Opening {path} for writing"))?;
1022 serde_json::to_writer(&mut out, &obj).context("Serializing output")?;
1023 }
1024 Ok(())
1025}
1026
1027pub async fn run_from_iter<I>(args: I) -> Result<()>
1030where
1031 I: IntoIterator,
1032 I::Item: Into<OsString> + Clone,
1033{
1034 run_from_opt(Opt::parse_from(args)).await
1035}
1036
1037async fn run_from_opt(opt: Opt) -> Result<()> {
1038 match opt {
1039 Opt::Tar(TarOpts::Import(ref opt)) => tar_import(opt).await,
1040 Opt::Tar(TarOpts::Export(ref opt)) => tar_export(opt),
1041 Opt::Container(o) => match o {
1042 ContainerOpts::Info { imgref } => container_info(&imgref).await,
1043 ContainerOpts::Commit {} => container_commit().await,
1044 ContainerOpts::Unencapsulate {
1045 repo,
1046 imgref,
1047 proxyopts,
1048 write_ref,
1049 quiet,
1050 } => {
1051 let repo = parse_repo(&repo)?;
1052 container_import(&repo, &imgref, proxyopts, write_ref.as_deref(), quiet).await
1053 }
1054 ContainerOpts::Encapsulate {
1055 repo,
1056 rev,
1057 imgref,
1058 labels,
1059 authfile,
1060 copy_meta_keys,
1061 copy_meta_opt_keys,
1062 config,
1063 cmd,
1064 compression_fast,
1065 contentmeta,
1066 } => {
1067 let labels: Result<BTreeMap<_, _>> = labels
1068 .into_iter()
1069 .map(|l| {
1070 let (k, v) = l
1071 .split_once('=')
1072 .ok_or_else(|| anyhow::anyhow!("Missing '=' in label {}", l))?;
1073 Ok((k.to_string(), v.to_string()))
1074 })
1075 .collect();
1076 let repo = parse_repo(&repo)?;
1077 container_export(
1078 &repo,
1079 &rev,
1080 &imgref,
1081 labels?,
1082 authfile,
1083 copy_meta_keys,
1084 copy_meta_opt_keys,
1085 config,
1086 cmd,
1087 compression_fast,
1088 contentmeta,
1089 )
1090 .await
1091 }
1092 ContainerOpts::Image(opts) => match opts {
1093 ContainerImageOpts::List { repo } => {
1094 let repo = parse_repo(&repo)?;
1095 for image in crate::container::store::list_images(&repo)? {
1096 println!("{}", image);
1097 }
1098 Ok(())
1099 }
1100 ContainerImageOpts::Pull {
1101 repo,
1102 imgref,
1103 proxyopts,
1104 quiet,
1105 check,
1106 } => {
1107 let repo = parse_repo(&repo)?;
1108 container_store(&repo, &imgref, proxyopts, quiet, check).await
1109 }
1110 ContainerImageOpts::Reexport {
1111 repo,
1112 src_imgref,
1113 dest_imgref,
1114 authfile,
1115 compression_fast,
1116 } => {
1117 let repo = &parse_repo(&repo)?;
1118 let opts = ExportToOCIOpts {
1119 authfile,
1120 skip_compression: compression_fast,
1121 ..Default::default()
1122 };
1123 let digest = ostree_container::store::export(
1124 repo,
1125 &src_imgref,
1126 &dest_imgref,
1127 Some(opts),
1128 )
1129 .await?;
1130 println!("Exported: {digest}");
1131 Ok(())
1132 }
1133 ContainerImageOpts::History { repo, imgref } => {
1134 let repo = parse_repo(&repo)?;
1135 container_history(&repo, &imgref).await
1136 }
1137 ContainerImageOpts::Metadata {
1138 repo,
1139 imgref,
1140 config,
1141 } => {
1142 let repo = parse_repo(&repo)?;
1143 let image = crate::container::store::query_image(&repo, &imgref)?
1144 .ok_or_else(|| anyhow::anyhow!("No such image"))?;
1145 let stdout = std::io::stdout().lock();
1146 let mut stdout = std::io::BufWriter::new(stdout);
1147 if config {
1148 serde_json::to_writer(&mut stdout, &image.configuration)?;
1149 } else {
1150 serde_json::to_writer(&mut stdout, &image.manifest)?;
1151 }
1152 stdout.flush()?;
1153 Ok(())
1154 }
1155 ContainerImageOpts::Remove {
1156 repo,
1157 imgrefs,
1158 skip_gc,
1159 } => {
1160 let nimgs = imgrefs.len();
1161 let repo = parse_repo(&repo)?;
1162 crate::container::store::remove_images(&repo, imgrefs.iter())?;
1163 if !skip_gc {
1164 let nlayers = crate::container::store::gc_image_layers(&repo)?;
1165 println!("Removed images: {nimgs} layers: {nlayers}");
1166 } else {
1167 println!("Removed images: {nimgs}");
1168 }
1169 Ok(())
1170 }
1171 ContainerImageOpts::PruneLayers { repo } => {
1172 let repo = parse_repo(&repo)?;
1173 let nlayers = crate::container::store::gc_image_layers(&repo)?;
1174 println!("Removed layers: {nlayers}");
1175 Ok(())
1176 }
1177 ContainerImageOpts::PruneImages {
1178 sysroot,
1179 and_layers,
1180 full,
1181 } => {
1182 let sysroot = &ostree::Sysroot::new(Some(&gio::File::for_path(&sysroot)));
1183 sysroot.load(gio::Cancellable::NONE)?;
1184 let sysroot = &SysrootLock::new_from_sysroot(sysroot).await?;
1185 if full {
1186 let res = crate::container::deploy::prune(sysroot)?;
1187 if res.is_empty() {
1188 println!("No content was pruned.");
1189 } else {
1190 println!("Removed images: {}", res.n_images);
1191 println!("Removed layers: {}", res.n_layers);
1192 println!("Removed objects: {}", res.n_objects_pruned);
1193 let objsize = glib::format_size(res.objsize);
1194 println!("Freed: {objsize}");
1195 }
1196 } else {
1197 let removed = crate::container::deploy::remove_undeployed_images(sysroot)?;
1198 match removed.as_slice() {
1199 [] => {
1200 println!("No unreferenced images.");
1201 return Ok(());
1202 }
1203 o => {
1204 for imgref in o {
1205 println!("Removed: {imgref}");
1206 }
1207 }
1208 }
1209 if and_layers {
1210 let nlayers =
1211 crate::container::store::gc_image_layers(&sysroot.repo())?;
1212 println!("Removed layers: {nlayers}");
1213 }
1214 }
1215 Ok(())
1216 }
1217 ContainerImageOpts::Copy {
1218 src_repo,
1219 dest_repo,
1220 imgref,
1221 } => {
1222 let src_repo = parse_repo(&src_repo)?;
1223 let dest_repo = parse_repo(&dest_repo)?;
1224 let imgref = &imgref.imgref;
1225 crate::container::store::copy(&src_repo, imgref, &dest_repo, imgref).await
1226 }
1227 ContainerImageOpts::ReplaceDetachedMetadata {
1228 src,
1229 dest,
1230 contents,
1231 } => {
1232 let contents = contents.map(std::fs::read).transpose()?;
1233 let digest = crate::container::update_detached_metadata(
1234 &src,
1235 &dest,
1236 contents.as_deref(),
1237 )
1238 .await?;
1239 println!("Pushed: {}", digest);
1240 Ok(())
1241 }
1242 ContainerImageOpts::Deploy {
1243 sysroot,
1244 stateroot,
1245 imgref,
1246 image,
1247 transport,
1248 mut no_signature_verification,
1249 enforce_container_sigpolicy,
1250 ostree_remote,
1251 target_imgref,
1252 no_imgref,
1253 karg,
1254 proxyopts,
1255 write_commitid_to,
1256 } => {
1257 no_signature_verification = !enforce_container_sigpolicy;
1260 let sysroot = &if let Some(sysroot) = sysroot {
1261 ostree::Sysroot::new(Some(&gio::File::for_path(sysroot)))
1262 } else {
1263 ostree::Sysroot::new_default()
1264 };
1265 sysroot.load(gio::Cancellable::NONE)?;
1266 let repo = &sysroot.repo();
1267 let kargs = karg.as_deref();
1268 let kargs = kargs.map(|v| {
1269 let r: Vec<_> = v.iter().map(|s| s.as_str()).collect();
1270 r
1271 });
1272
1273 let stateroot = if let Some(stateroot) = stateroot.as_deref() {
1275 Cow::Borrowed(stateroot)
1276 } else {
1277 let booted_stateroot = sysroot
1280 .booted_deployment()
1281 .map(|d| Cow::Owned(d.osname().to_string()));
1282 booted_stateroot.unwrap_or({
1283 Cow::Borrowed(crate::container::deploy::STATEROOT_DEFAULT)
1284 })
1285 };
1286
1287 let imgref = if let Some(image) = image {
1288 let transport = transport.as_deref().unwrap_or("registry");
1289 let transport = ostree_container::Transport::try_from(transport)?;
1290 let imgref = ostree_container::ImageReference {
1291 transport,
1292 name: image,
1293 };
1294 let sigverify = if no_signature_verification {
1295 ostree_container::SignatureSource::ContainerPolicyAllowInsecure
1296 } else if let Some(remote) = ostree_remote.as_ref() {
1297 ostree_container::SignatureSource::OstreeRemote(remote.to_string())
1298 } else {
1299 ostree_container::SignatureSource::ContainerPolicy
1300 };
1301 ostree_container::OstreeImageReference { sigverify, imgref }
1302 } else {
1303 let imgref = imgref.expect("imgref option should be set");
1306 imgref.as_str().try_into()?
1307 };
1308
1309 #[allow(clippy::needless_update)]
1310 let options = crate::container::deploy::DeployOpts {
1311 kargs: kargs.as_deref(),
1312 target_imgref: target_imgref.as_ref(),
1313 proxy_cfg: Some(proxyopts.into()),
1314 no_imgref,
1315 ..Default::default()
1316 };
1317 let state = crate::container::deploy::deploy(
1318 sysroot,
1319 &stateroot,
1320 &imgref,
1321 Some(options),
1322 )
1323 .await?;
1324 let wrote_imgref = target_imgref.as_ref().unwrap_or(&imgref);
1325 if let Some(msg) = ostree_container::store::image_filtered_content_warning(
1326 repo,
1327 &wrote_imgref.imgref,
1328 )? {
1329 eprintln!("{msg}")
1330 }
1331 if let Some(p) = write_commitid_to {
1332 std::fs::write(&p, state.merge_commit.as_bytes())
1333 .with_context(|| format!("Failed to write commitid to {}", p))?;
1334 }
1335 Ok(())
1336 }
1337 },
1338 ContainerOpts::Compare {
1339 imgref_old,
1340 imgref_new,
1341 } => {
1342 let (manifest_old, _) = crate::container::fetch_manifest(&imgref_old).await?;
1343 let (manifest_new, _) = crate::container::fetch_manifest(&imgref_new).await?;
1344 let manifest_diff =
1345 crate::container::ManifestDiff::new(&manifest_old, &manifest_new);
1346 manifest_diff.print();
1347 Ok(())
1348 }
1349 },
1350 Opt::ImaSign(ref opts) => ima_sign(opts),
1351 #[cfg(feature = "internal-testing-api")]
1352 Opt::InternalOnlyForTesting(ref opts) => testing(opts).await,
1353 #[cfg(feature = "docgen")]
1354 Opt::Man(manopts) => crate::docgen::generate_manpages(&manopts.directory),
1355 Opt::ProvisionalRepair(opts) => match opts {
1356 ProvisionalRepairOpts::AnalyzeInodes {
1357 repo,
1358 verbose,
1359 write_result_to,
1360 } => {
1361 let repo = parse_repo(&repo)?;
1362 let check_res = crate::repair::check_inode_collision(&repo, verbose)?;
1363 handle_serialize_to_file(write_result_to.as_deref(), &check_res)?;
1364 if check_res.collisions.is_empty() {
1365 println!("OK: No colliding objects found.");
1366 } else {
1367 eprintln!(
1368 "warning: {} potentially colliding inodes found",
1369 check_res.collisions.len()
1370 );
1371 }
1372 Ok(())
1373 }
1374 ProvisionalRepairOpts::Repair {
1375 sysroot,
1376 verbose,
1377 dry_run,
1378 write_result_to,
1379 } => {
1380 container_remount_sysroot(&sysroot)?;
1381 let sysroot = &ostree::Sysroot::new(Some(&gio::File::for_path(&sysroot)));
1382 sysroot.load(gio::Cancellable::NONE)?;
1383 let sysroot = &SysrootLock::new_from_sysroot(sysroot).await?;
1384 let result = crate::repair::analyze_for_repair(sysroot, verbose)?;
1385 handle_serialize_to_file(write_result_to.as_deref(), &result)?;
1386 if dry_run {
1387 result.check()
1388 } else {
1389 result.repair(sysroot)
1390 }
1391 }
1392 },
1393 }
1394}