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