1use std::ffi::{CString, OsStr, OsString};
6use std::fs::File;
7use std::io::{BufWriter, Seek, SeekFrom};
8use std::os::fd::AsFd;
9use std::os::unix::process::CommandExt;
10use std::process::Command;
11
12use anyhow::{Context, Result, anyhow, ensure};
13use camino::{Utf8Path, Utf8PathBuf};
14use cap_std_ext::cap_std;
15use cap_std_ext::cap_std::fs::Dir;
16use clap::CommandFactory;
17use clap::Parser;
18use clap::ValueEnum;
19use composefs::dumpfile;
20use composefs::fsverity;
21use composefs::fsverity::FsVerityHashValue;
22use composefs_ctl::composefs;
23use composefs_ctl::composefs_boot;
24use composefs_ctl::composefs_oci;
25
26use composefs_boot::BootOps as _;
27use etc_merge::{compute_diff, print_diff};
28use fn_error_context::context;
29use indoc::indoc;
30use ocidir::cap_std::ambient_authority;
31use ostree::gio;
32use ostree_container::store::PrepareResult;
33use ostree_ext::container as ostree_container;
34
35use ostree_ext::keyfileext::KeyFileExt;
36use ostree_ext::ostree;
37use ostree_ext::sysroot::SysrootLock;
38use schemars::schema_for;
39use serde::{Deserialize, Serialize};
40
41use crate::bootc_composefs::delete::delete_composefs_deployment;
42use crate::bootc_composefs::gc::{GCOpts, composefs_gc};
43use crate::bootc_composefs::soft_reboot::{prepare_soft_reboot_composefs, reset_soft_reboot};
44use crate::bootc_composefs::{
45 digest::{compute_composefs_digest, new_temp_composefs_repo},
46 finalize::{composefs_backend_finalize, get_etc_diff},
47 rollback::composefs_rollback,
48 state::composefs_usr_overlay,
49 switch::switch_composefs,
50 update::upgrade_composefs,
51};
52use crate::deploy::{MergeState, RequiredHostSpec};
53use crate::podstorage::set_additional_image_store;
54use crate::progress_jsonl::{ProgressWriter, RawProgressFd};
55use crate::spec::FilesystemOverlayAccessMode;
56use crate::spec::Host;
57use crate::spec::ImageReference;
58use crate::status::get_host;
59use crate::store::{BootedOstree, Storage};
60use crate::store::{BootedStorage, BootedStorageKind};
61use crate::utils::sigpolicy_from_opt;
62use crate::{bootc_composefs, lints};
63
64#[derive(Debug, Parser, PartialEq, Eq)]
66pub(crate) struct ProgressOptions {
67 #[clap(long, hide = true)]
71 pub(crate) progress_fd: Option<RawProgressFd>,
72}
73
74impl TryFrom<ProgressOptions> for ProgressWriter {
75 type Error = anyhow::Error;
76
77 fn try_from(value: ProgressOptions) -> Result<Self> {
78 let r = value
79 .progress_fd
80 .map(TryInto::try_into)
81 .transpose()?
82 .unwrap_or_default();
83 Ok(r)
84 }
85}
86
87#[derive(Debug, Parser, PartialEq, Eq)]
89pub(crate) struct UpgradeOpts {
90 #[clap(long)]
92 pub(crate) quiet: bool,
93
94 #[clap(long, conflicts_with = "apply")]
98 pub(crate) check: bool,
99
100 #[clap(long, conflicts_with = "check")]
104 pub(crate) apply: bool,
105
106 #[clap(long = "soft-reboot", conflicts_with = "check")]
110 pub(crate) soft_reboot: Option<SoftRebootMode>,
111
112 #[clap(long, conflicts_with_all = ["check", "apply"])]
118 pub(crate) download_only: bool,
119
120 #[clap(long, conflicts_with_all = ["check", "download_only"])]
126 pub(crate) from_downloaded: bool,
127
128 #[clap(long)]
133 pub(crate) tag: Option<String>,
134
135 #[clap(flatten)]
136 pub(crate) progress: ProgressOptions,
137}
138
139#[derive(Debug, Parser, PartialEq, Eq)]
141pub(crate) struct SwitchOpts {
142 #[clap(long)]
144 pub(crate) quiet: bool,
145
146 #[clap(long)]
150 pub(crate) apply: bool,
151
152 #[clap(long = "soft-reboot")]
156 pub(crate) soft_reboot: Option<SoftRebootMode>,
157
158 #[clap(long, default_value = "registry")]
160 pub(crate) transport: String,
161
162 #[clap(long, hide = true)]
164 pub(crate) no_signature_verification: bool,
165
166 #[clap(long)]
173 pub(crate) enforce_container_sigpolicy: bool,
174
175 #[clap(long, hide = true)]
179 pub(crate) mutate_in_place: bool,
180
181 #[clap(long)]
183 pub(crate) retain: bool,
184
185 #[clap(long = "experimental-unified-storage", hide = true)]
191 pub(crate) unified_storage_exp: bool,
192
193 pub(crate) target: String,
195
196 #[clap(flatten)]
197 pub(crate) progress: ProgressOptions,
198}
199
200#[derive(Debug, Parser, PartialEq, Eq)]
202pub(crate) struct RollbackOpts {
203 #[clap(long)]
209 pub(crate) apply: bool,
210
211 #[clap(long = "soft-reboot")]
215 pub(crate) soft_reboot: Option<SoftRebootMode>,
216}
217
218#[derive(Debug, Parser, PartialEq, Eq)]
220pub(crate) struct EditOpts {
221 #[clap(long, short = 'f')]
223 pub(crate) filename: Option<String>,
224
225 #[clap(long)]
227 pub(crate) quiet: bool,
228}
229
230#[derive(Debug, Clone, ValueEnum, PartialEq, Eq)]
231#[clap(rename_all = "lowercase")]
232pub(crate) enum OutputFormat {
233 HumanReadable,
235 Yaml,
237 Json,
239}
240
241#[derive(Debug, Clone, Copy, ValueEnum, PartialEq, Eq)]
242#[clap(rename_all = "lowercase")]
243pub(crate) enum SoftRebootMode {
244 Required,
246 Auto,
248}
249
250#[derive(Debug, Parser, PartialEq, Eq)]
252pub(crate) struct StatusOpts {
253 #[clap(long, hide = true)]
257 pub(crate) json: bool,
258
259 #[clap(long)]
261 pub(crate) format: Option<OutputFormat>,
262
263 #[clap(long)]
268 pub(crate) format_version: Option<u32>,
269
270 #[clap(long)]
272 pub(crate) booted: bool,
273
274 #[clap(long, short = 'v')]
276 pub(crate) verbose: bool,
277}
278
279#[derive(Debug, Parser, PartialEq, Eq)]
281pub(crate) struct UsrOverlayOpts {
282 #[clap(long)]
286 pub(crate) read_only: bool,
287}
288
289#[derive(Debug, clap::Subcommand, PartialEq, Eq)]
290pub(crate) enum InstallOpts {
291 #[cfg(feature = "install-to-disk")]
302 ToDisk(crate::install::InstallToDiskOpts),
303 ToFilesystem(crate::install::InstallToFilesystemOpts),
310 ToExistingRoot(crate::install::InstallToExistingRootOpts),
317 #[clap(hide = true)]
322 Reset(crate::install::InstallResetOpts),
323 Finalize {
326 root_path: Utf8PathBuf,
328 },
329 EnsureCompletion {},
337 PrintConfiguration(crate::install::InstallPrintConfigurationOpts),
344}
345
346#[derive(Debug, clap::Subcommand, PartialEq, Eq)]
348pub(crate) enum ContainerOpts {
349 Inspect {
354 #[clap(long, default_value = "/")]
356 rootfs: Utf8PathBuf,
357
358 #[clap(long)]
360 json: bool,
361
362 #[clap(long, conflicts_with = "json")]
364 format: Option<OutputFormat>,
365 },
366 Lint {
372 #[clap(long, default_value = "/")]
374 rootfs: Utf8PathBuf,
375
376 #[clap(long)]
378 fatal_warnings: bool,
379
380 #[clap(long)]
385 list: bool,
386
387 #[clap(long)]
392 skip: Vec<String>,
393
394 #[clap(long)]
397 no_truncate: bool,
398 },
399 #[clap(hide = true)]
401 ComputeComposefsDigest {
402 #[clap(default_value = "/target")]
404 path: Utf8PathBuf,
405
406 #[clap(long)]
408 write_dumpfile_to: Option<Utf8PathBuf>,
409 },
410 #[clap(hide = true)]
412 ComputeComposefsDigestFromStorage {
413 #[clap(long)]
415 write_dumpfile_to: Option<Utf8PathBuf>,
416
417 image: Option<String>,
419 },
420 SplitKernelAndRootfs {
429 #[clap(long, default_value = "/")]
431 rootfs: Utf8PathBuf,
432
433 #[clap(long)]
435 output: Utf8PathBuf,
436 },
437 Ukify {
446 #[clap(long, default_value = "/")]
448 rootfs: Utf8PathBuf,
449
450 #[clap(long = "karg", hide = true)]
454 kargs: Vec<String>,
455
456 #[clap(long)]
458 allow_missing_verity: bool,
459
460 #[clap(long)]
462 write_dumpfile_to: Option<Utf8PathBuf>,
463
464 #[clap(long)]
469 kernel_dir: Option<Utf8PathBuf>,
470
471 #[clap(last = true)]
473 args: Vec<OsString>,
474 },
475 #[clap(hide = true)]
483 Export {
484 #[clap(long, default_value = "tar")]
486 format: ExportFormat,
487
488 #[clap(long, short = 'o')]
490 output: Option<Utf8PathBuf>,
491
492 #[clap(long)]
495 kernel_in_boot: bool,
496
497 #[clap(long)]
499 disable_selinux: bool,
500
501 target: Utf8PathBuf,
503 },
504}
505
506#[derive(Debug, Clone, ValueEnum, PartialEq, Eq)]
507pub(crate) enum ExportFormat {
508 Tar,
510}
511
512#[derive(Debug, clap::Subcommand, PartialEq, Eq)]
514pub(crate) enum ImageCmdOpts {
515 List {
517 #[clap(allow_hyphen_values = true)]
518 args: Vec<OsString>,
519 },
520 Build {
522 #[clap(allow_hyphen_values = true)]
523 args: Vec<OsString>,
524 },
525 Pull {
527 #[clap(required = true)]
529 images: Vec<String>,
530 },
531 Push {
533 #[clap(allow_hyphen_values = true)]
534 args: Vec<OsString>,
535 },
536}
537
538#[derive(ValueEnum, Debug, Copy, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
539#[serde(rename_all = "kebab-case")]
540pub(crate) enum ImageListType {
541 #[default]
543 All,
544 Logical,
546 Host,
548}
549
550impl std::fmt::Display for ImageListType {
551 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
552 self.to_possible_value().unwrap().get_name().fmt(f)
553 }
554}
555
556#[derive(ValueEnum, Debug, Copy, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
557#[serde(rename_all = "kebab-case")]
558pub(crate) enum ImageListFormat {
559 #[default]
561 Table,
562 Json,
564}
565impl std::fmt::Display for ImageListFormat {
566 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
567 self.to_possible_value().unwrap().get_name().fmt(f)
568 }
569}
570
571#[derive(Debug, clap::Subcommand, PartialEq, Eq)]
573pub(crate) enum ImageOpts {
574 List {
578 #[clap(long = "type")]
580 #[arg(default_value_t)]
581 list_type: ImageListType,
582 #[clap(long = "format")]
583 #[arg(default_value_t)]
584 list_format: ImageListFormat,
585 },
586 CopyToStorage {
603 #[clap(long)]
604 source: Option<String>,
606
607 #[clap(long)]
608 target: Option<String>,
611 },
612 SetUnified,
617 PullFromDefaultStorage {
619 image: String,
621 },
622 #[clap(subcommand)]
624 Cmd(ImageCmdOpts),
625}
626
627#[derive(Debug, Clone, clap::ValueEnum, PartialEq, Eq)]
628pub(crate) enum SchemaType {
629 Host,
630 Progress,
631}
632
633#[derive(Debug, clap::Subcommand, PartialEq, Eq)]
635pub(crate) enum FsverityOpts {
636 Measure {
638 path: Utf8PathBuf,
640 },
641 Enable {
643 path: Utf8PathBuf,
645 },
646}
647
648#[derive(Debug, clap::Subcommand, PartialEq, Eq)]
649pub(crate) enum UkiSubcommands {
650 Extract {
654 path: Utf8PathBuf,
656 output_path: Utf8PathBuf,
658 },
659}
660
661#[derive(Debug, clap::Subcommand, PartialEq, Eq)]
663pub(crate) enum InternalsOpts {
664 SystemdGenerator {
665 normal_dir: Utf8PathBuf,
666 #[allow(dead_code)]
667 early_dir: Option<Utf8PathBuf>,
668 #[allow(dead_code)]
669 late_dir: Option<Utf8PathBuf>,
670 },
671 FixupEtcFstab,
672 SysusersSync,
675 PrintJsonSchema {
677 #[clap(long)]
678 of: SchemaType,
679 },
680 #[clap(subcommand)]
681 Fsverity(FsverityOpts),
682 Fsck,
684 Cleanup,
686 Relabel {
687 #[clap(long)]
688 as_path: Option<Utf8PathBuf>,
690
691 path: Utf8PathBuf,
693 },
694 RelabelOverlayMountpoints,
697 OstreeExt {
699 #[clap(allow_hyphen_values = true)]
700 args: Vec<OsString>,
701 },
702 Cfs {
704 #[clap(allow_hyphen_values = true)]
705 args: Vec<OsString>,
706 },
707 OstreeContainer {
709 #[clap(allow_hyphen_values = true)]
710 args: Vec<OsString>,
711 },
712 TestComposefs,
714 LoopbackCleanupHelper {
716 #[clap(long)]
718 device: String,
719 },
720 AllocateCleanupLoopback {
722 #[clap(long)]
724 file_path: Utf8PathBuf,
725 },
726 BootcInstallCompletion {
728 sysroot: Utf8PathBuf,
730
731 stateroot: String,
733 },
734 Reboot,
737 #[cfg(feature = "rhsm")]
738 PublishRhsmFacts,
740 DirDiff {
742 pristine_etc: Utf8PathBuf,
744 current_etc: Utf8PathBuf,
746 new_etc: Utf8PathBuf,
748 #[clap(long)]
750 merge: bool,
751 },
752 #[cfg(feature = "docgen")]
753 DumpCliJson,
755 PrepSoftReboot {
756 #[clap(required_unless_present = "reset")]
757 deployment: Option<String>,
758 #[clap(long, conflicts_with = "reset")]
759 reboot: bool,
760 #[clap(long, conflicts_with = "reboot")]
761 reset: bool,
762 },
763 ComposefsGC {
764 #[clap(long)]
765 dry_run: bool,
766 #[clap(long)]
769 assert_no_op: bool,
770 #[clap(long)]
772 prune_repo: bool,
773 },
774 #[clap(subcommand)]
776 Blockdev(BlockdevOpts),
777 #[clap(subcommand)]
779 Uki(UkiSubcommands),
780}
781
782#[derive(Debug, clap::Subcommand, PartialEq, Eq)]
784pub(crate) enum BlockdevOpts {
785 Ls {
791 device: Utf8PathBuf,
793 },
794 LsFilesystem {
799 path: Utf8PathBuf,
801 },
802}
803
804#[derive(Debug, Parser, PartialEq, Eq)]
806pub(crate) struct SetOptionsForSourceOpts {
807 #[clap(long)]
812 pub(crate) source: String,
813
814 #[clap(long)]
819 pub(crate) options: Option<String>,
820}
821
822#[derive(Debug, clap::Subcommand, PartialEq, Eq)]
830pub(crate) enum LoaderEntriesOpts {
831 SetOptionsForSource(SetOptionsForSourceOpts),
850}
851
852#[derive(Debug, clap::Subcommand, PartialEq, Eq)]
853pub(crate) enum StateOpts {
854 WipeOstree,
856}
857
858impl InternalsOpts {
859 const GENERATOR_BIN: &'static str = "bootc-systemd-generator";
861}
862
863#[derive(Debug, Parser, PartialEq, Eq)]
871#[clap(name = "bootc")]
872#[clap(rename_all = "kebab-case")]
873#[clap(version,long_version=clap::crate_version!())]
874#[allow(clippy::large_enum_variant)]
875pub(crate) enum Opt {
876 #[clap(alias = "update")]
889 Upgrade(UpgradeOpts),
890 Switch(SwitchOpts),
901 #[command(after_help = indoc! {r#"
913 Note on Rollbacks and the `/etc` Directory:
914
915 When you perform a rollback (e.g., with `bootc rollback`), any
916 changes made to files in the `/etc` directory won't carry over
917 to the rolled-back deployment. The `/etc` files will revert
918 to their state from that previous deployment instead.
919
920 This is because `bootc rollback` just reorders the existing
921 deployments. It doesn't create new deployments. The `/etc`
922 merges happen when new deployments are created.
923 "#})]
924 Rollback(RollbackOpts),
925 Edit(EditOpts),
935 Status(StatusOpts),
939 #[clap(alias = "usroverlay")]
943 UsrOverlay(UsrOverlayOpts),
944 #[clap(subcommand)]
948 Install(InstallOpts),
949 #[clap(subcommand)]
951 Container(ContainerOpts),
952 #[clap(subcommand, hide = true)]
956 Image(ImageOpts),
957 #[clap(subcommand)]
961 LoaderEntries(LoaderEntriesOpts),
962 #[clap(hide = true)]
964 ExecInHostMountNamespace {
965 #[clap(trailing_var_arg = true, allow_hyphen_values = true)]
966 args: Vec<OsString>,
967 },
968 #[clap(hide = true)]
970 #[clap(subcommand)]
971 State(StateOpts),
972 #[clap(subcommand)]
973 #[clap(hide = true)]
974 Internals(InternalsOpts),
975 ComposefsFinalizeStaged,
976 #[clap(hide = true)]
978 ConfigDiff,
979 #[clap(hide = true)]
983 Completion {
984 #[clap(value_enum)]
986 shell: clap_complete::aot::Shell,
987 },
988 #[clap(hide = true)]
989 DeleteDeployment {
990 depl_id: String,
991 },
992}
993
994#[context("Ensuring mountns")]
999pub(crate) fn ensure_self_unshared_mount_namespace() -> Result<()> {
1000 let uid = rustix::process::getuid();
1001 if !uid.is_root() {
1002 tracing::debug!("Not root, assuming no need to unshare");
1003 return Ok(());
1004 }
1005 let recurse_env = "_ostree_unshared";
1006 let ns_pid1 = std::fs::read_link("/proc/1/ns/mnt").context("Reading /proc/1/ns/mnt")?;
1007 let ns_self = std::fs::read_link("/proc/self/ns/mnt").context("Reading /proc/self/ns/mnt")?;
1008 if ns_pid1 != ns_self {
1010 tracing::debug!("Already in a mount namespace");
1011 return Ok(());
1012 }
1013 if std::env::var_os(recurse_env).is_some() {
1014 let am_pid1 = rustix::process::getpid().is_init();
1015 if am_pid1 {
1016 tracing::debug!("We are pid 1");
1017 return Ok(());
1018 } else {
1019 anyhow::bail!("Failed to unshare mount namespace");
1020 }
1021 }
1022 bootc_utils::reexec::reexec_with_guardenv(recurse_env, &["unshare", "-m", "--"])
1023}
1024
1025#[context("Initializing storage")]
1028pub(crate) async fn get_storage() -> Result<crate::store::BootedStorage> {
1029 let env = crate::store::Environment::detect()?;
1030 prepare_for_write()?;
1033 let r = BootedStorage::new(env, crate::store::EspAccess::ReadWrite)
1034 .await?
1035 .ok_or_else(|| anyhow!("System not booted via bootc"))?;
1036 r.require_writable()?;
1037 Ok(r)
1038}
1039
1040#[context("Querying root privilege")]
1041pub(crate) fn require_root(is_container: bool) -> Result<()> {
1042 ensure!(
1043 rustix::process::getuid().is_root(),
1044 if is_container {
1045 "The user inside the container from which you are running this command must be root"
1046 } else {
1047 "This command must be executed as the root user"
1048 }
1049 );
1050
1051 ensure!(
1052 rustix::thread::capability_is_in_bounding_set(rustix::thread::CapabilitySet::SYS_ADMIN)?,
1053 if is_container {
1054 "The container must be executed with full privileges (e.g. --privileged flag)"
1055 } else {
1056 "This command requires full root privileges (CAP_SYS_ADMIN)"
1057 }
1058 );
1059
1060 tracing::trace!("Verified uid 0 with CAP_SYS_ADMIN");
1061
1062 Ok(())
1063}
1064
1065fn has_soft_reboot_capability(deployment: Option<&crate::spec::BootEntry>) -> bool {
1067 deployment.map(|d| d.soft_reboot_capable).unwrap_or(false)
1068}
1069
1070#[context("Preparing soft reboot")]
1072fn prepare_soft_reboot(sysroot: &SysrootLock, deployment: &ostree::Deployment) -> Result<()> {
1073 let cancellable = ostree::gio::Cancellable::NONE;
1074 sysroot
1075 .deployment_set_soft_reboot(deployment, false, cancellable)
1076 .context("Failed to prepare soft-reboot")?;
1077 Ok(())
1078}
1079
1080#[context("Handling soft reboot")]
1082fn handle_soft_reboot<F>(
1083 soft_reboot_mode: Option<SoftRebootMode>,
1084 entry: Option<&crate::spec::BootEntry>,
1085 deployment_type: &str,
1086 execute_soft_reboot: F,
1087) -> Result<()>
1088where
1089 F: FnOnce() -> Result<()>,
1090{
1091 let Some(mode) = soft_reboot_mode else {
1092 return Ok(());
1093 };
1094
1095 let can_soft_reboot = has_soft_reboot_capability(entry);
1096 match mode {
1097 SoftRebootMode::Required => {
1098 if can_soft_reboot {
1099 execute_soft_reboot()?;
1100 } else {
1101 anyhow::bail!(
1102 "Soft reboot was required but {} deployment is not soft-reboot capable",
1103 deployment_type
1104 );
1105 }
1106 }
1107 SoftRebootMode::Auto => {
1108 if can_soft_reboot {
1109 execute_soft_reboot()?;
1110 }
1111 }
1112 }
1113 Ok(())
1114}
1115
1116#[context("Handling staged soft reboot")]
1118fn handle_staged_soft_reboot(
1119 booted_ostree: &BootedOstree<'_>,
1120 soft_reboot_mode: Option<SoftRebootMode>,
1121 host: &crate::spec::Host,
1122) -> Result<()> {
1123 handle_soft_reboot(
1124 soft_reboot_mode,
1125 host.status.staged.as_ref(),
1126 "staged",
1127 || soft_reboot_staged(booted_ostree.sysroot),
1128 )
1129}
1130
1131#[context("Soft reboot staged deployment")]
1133fn soft_reboot_staged(sysroot: &SysrootLock) -> Result<()> {
1134 println!("Staged deployment is soft-reboot capable, preparing for soft-reboot...");
1135
1136 let deployments_list = sysroot.deployments();
1137 let staged_deployment = deployments_list
1138 .iter()
1139 .find(|d| d.is_staged())
1140 .ok_or_else(|| anyhow::anyhow!("Failed to find staged deployment"))?;
1141
1142 prepare_soft_reboot(sysroot, staged_deployment)?;
1143 Ok(())
1144}
1145
1146#[context("Soft reboot rollback deployment")]
1148fn soft_reboot_rollback(booted_ostree: &BootedOstree<'_>) -> Result<()> {
1149 println!("Rollback deployment is soft-reboot capable, preparing for soft-reboot...");
1150
1151 let deployments_list = booted_ostree.sysroot.deployments();
1152 let target_deployment = deployments_list
1153 .first()
1154 .ok_or_else(|| anyhow::anyhow!("No rollback deployment found!"))?;
1155
1156 prepare_soft_reboot(booted_ostree.sysroot, target_deployment)
1157}
1158
1159#[context("Preparing for write")]
1163pub(crate) fn prepare_for_write() -> Result<()> {
1164 use std::sync::atomic::{AtomicBool, Ordering};
1165
1166 static ENTERED: AtomicBool = AtomicBool::new(false);
1172 if ENTERED.load(Ordering::SeqCst) {
1173 return Ok(());
1174 }
1175 if ostree_ext::container_utils::running_in_container() {
1176 anyhow::bail!("Detected container; this command requires a booted host system.");
1177 }
1178 crate::cli::require_root(false)?;
1179 ensure_self_unshared_mount_namespace()?;
1180 if crate::lsm::selinux_enabled()? && !crate::lsm::selinux_ensure_install()? {
1181 tracing::debug!("Do not have install_t capabilities");
1182 }
1183 ENTERED.store(true, Ordering::SeqCst);
1184 Ok(())
1185}
1186
1187#[context("Upgrading")]
1189async fn upgrade(
1190 opts: UpgradeOpts,
1191 storage: &Storage,
1192 booted_ostree: &BootedOstree<'_>,
1193) -> Result<()> {
1194 let repo = &booted_ostree.repo();
1195
1196 let host = crate::status::get_status(booted_ostree)?.1;
1197 let current_image = host.spec.image.as_ref();
1198
1199 let derived_image = if let Some(ref tag) = opts.tag {
1201 let image = current_image.ok_or_else(|| {
1202 anyhow::anyhow!("--tag requires a booted image with a specified source")
1203 })?;
1204 Some(image.with_tag(tag)?)
1205 } else {
1206 None
1207 };
1208
1209 let imgref = derived_image.as_ref().or(current_image);
1210 let prog: ProgressWriter = opts.progress.try_into()?;
1211
1212 if imgref.is_none() {
1214 let booted_incompatible = host.status.booted.as_ref().is_some_and(|b| b.incompatible);
1215
1216 let staged_incompatible = host.status.staged.as_ref().is_some_and(|b| b.incompatible);
1217
1218 if booted_incompatible || staged_incompatible {
1219 return Err(anyhow::anyhow!(
1220 "Deployment contains local rpm-ostree modifications; cannot upgrade via bootc. You can run `rpm-ostree reset` to undo the modifications."
1221 ));
1222 }
1223 }
1224
1225 let imgref = imgref.ok_or_else(|| anyhow::anyhow!("No image source specified"))?;
1226 let spec = RequiredHostSpec { image: imgref };
1228 let booted_image = host
1229 .status
1230 .booted
1231 .as_ref()
1232 .map(|b| b.query_image(repo))
1233 .transpose()?
1234 .flatten();
1235 let staged = host.status.staged.as_ref();
1237 let staged_image = staged.as_ref().and_then(|s| s.image.as_ref());
1238 let mut changed = false;
1239
1240 if opts.from_downloaded {
1242 let ostree = storage.get_ostree()?;
1243 let staged_deployment = ostree
1244 .staged_deployment()
1245 .ok_or_else(|| anyhow::anyhow!("No staged deployment found"))?;
1246
1247 if staged_deployment.is_finalization_locked() {
1248 ostree.change_finalization(&staged_deployment)?;
1249 println!("Staged deployment will now be applied on reboot");
1250 } else {
1251 println!("Staged deployment is already set to apply on reboot");
1252 }
1253
1254 handle_staged_soft_reboot(booted_ostree, opts.soft_reboot, &host)?;
1255 if opts.apply {
1256 crate::reboot::reboot()?;
1257 }
1258 return Ok(());
1259 }
1260
1261 let use_unified = crate::deploy::image_exists_in_unified_storage(storage, imgref).await?;
1265
1266 if opts.check {
1267 let ostree_imgref = imgref.clone().into();
1268 let mut imp =
1269 crate::deploy::new_importer(repo, &ostree_imgref, Some(&booted_ostree.deployment))
1270 .await?;
1271 match imp.prepare().await? {
1272 PrepareResult::AlreadyPresent(_) => {
1273 println!("No changes in: {ostree_imgref:#}");
1274 }
1275 PrepareResult::Ready(r) => {
1276 crate::deploy::check_bootc_label(&r.config);
1277 println!("Update available for: {ostree_imgref:#}");
1278 if let Some(version) = r.version() {
1279 println!(" Version: {version}");
1280 }
1281 println!(" Digest: {}", r.manifest_digest);
1282 changed = true;
1283 if let Some(previous_image) = booted_image.as_ref() {
1284 let diff =
1285 ostree_container::ManifestDiff::new(&previous_image.manifest, &r.manifest);
1286 diff.print();
1287 }
1288 }
1289 }
1290 } else {
1291 let fetched = if use_unified {
1292 crate::deploy::pull_unified(
1293 repo,
1294 imgref,
1295 None,
1296 opts.quiet,
1297 prog.clone(),
1298 storage,
1299 Some(&booted_ostree.deployment),
1300 )
1301 .await?
1302 } else {
1303 crate::deploy::pull(
1304 repo,
1305 imgref,
1306 None,
1307 opts.quiet,
1308 prog.clone(),
1309 Some(&booted_ostree.deployment),
1310 )
1311 .await?
1312 };
1313 let staged_digest = staged_image.map(|s| s.digest().expect("valid digest in status"));
1314 let fetched_digest = &fetched.manifest_digest;
1315 tracing::debug!("staged: {staged_digest:?}");
1316 tracing::debug!("fetched: {fetched_digest}");
1317 let staged_unchanged = staged_digest
1318 .as_ref()
1319 .map(|d| d == fetched_digest)
1320 .unwrap_or_default();
1321 let booted_unchanged = booted_image
1322 .as_ref()
1323 .map(|img| &img.manifest_digest == fetched_digest)
1324 .unwrap_or_default();
1325 if staged_unchanged {
1326 let staged_deployment = storage.get_ostree()?.staged_deployment();
1327 let mut download_only_changed = false;
1328
1329 if let Some(staged) = staged_deployment {
1330 if opts.download_only {
1332 if !staged.is_finalization_locked() {
1334 storage.get_ostree()?.change_finalization(&staged)?;
1335 println!("Image downloaded, but will not be applied on reboot");
1336 download_only_changed = true;
1337 }
1338 } else if !opts.check {
1339 if staged.is_finalization_locked() {
1342 storage.get_ostree()?.change_finalization(&staged)?;
1343 println!("Staged deployment will now be applied on reboot");
1344 download_only_changed = true;
1345 }
1346 }
1347 } else if opts.download_only || opts.apply {
1348 anyhow::bail!("No staged deployment found");
1349 }
1350
1351 if !download_only_changed {
1352 println!("Staged update present, not changed");
1353 }
1354
1355 handle_staged_soft_reboot(booted_ostree, opts.soft_reboot, &host)?;
1356 if opts.apply {
1357 crate::reboot::reboot()?;
1358 }
1359 } else if booted_unchanged {
1360 println!("No update available.")
1361 } else {
1362 let stateroot = booted_ostree.stateroot();
1363 let from = MergeState::from_stateroot(storage, &stateroot)?;
1364 crate::deploy::stage(
1365 storage,
1366 from,
1367 &fetched,
1368 &spec,
1369 prog.clone(),
1370 opts.download_only,
1371 )
1372 .await?;
1373 changed = true;
1374 if let Some(prev) = booted_image.as_ref() {
1375 if let Some(fetched_manifest) = fetched.get_manifest(repo)? {
1376 let diff =
1377 ostree_container::ManifestDiff::new(&prev.manifest, &fetched_manifest);
1378 diff.print();
1379 }
1380 }
1381 }
1382 }
1383 if changed {
1384 storage.update_mtime()?;
1385
1386 if opts.soft_reboot.is_some() {
1387 let updated_host = crate::status::get_status(booted_ostree)?.1;
1390 handle_staged_soft_reboot(booted_ostree, opts.soft_reboot, &updated_host)?;
1391 }
1392
1393 if opts.apply {
1394 crate::reboot::reboot()?;
1395 }
1396 } else {
1397 tracing::debug!("No changes");
1398 }
1399
1400 Ok(())
1401}
1402pub(crate) fn imgref_for_switch(opts: &SwitchOpts) -> Result<ImageReference> {
1403 let transport = ostree_container::Transport::try_from(opts.transport.as_str())?;
1404 let imgref = ostree_container::ImageReference {
1405 transport,
1406 name: opts.target.to_string(),
1407 };
1408 let sigverify = sigpolicy_from_opt(opts.enforce_container_sigpolicy);
1409 let target = ostree_container::OstreeImageReference { sigverify, imgref };
1410 let target = ImageReference::from(target);
1411
1412 return Ok(target);
1413}
1414
1415#[context("Switching (ostree)")]
1417async fn switch_ostree(
1418 opts: SwitchOpts,
1419 storage: &Storage,
1420 booted_ostree: &BootedOstree<'_>,
1421) -> Result<()> {
1422 let target = imgref_for_switch(&opts)?;
1423 let prog: ProgressWriter = opts.progress.try_into()?;
1424 let cancellable = gio::Cancellable::NONE;
1425
1426 let repo = &booted_ostree.repo();
1427 let (_, host) = crate::status::get_status(booted_ostree)?;
1428
1429 let new_spec = {
1430 let mut new_spec = host.spec.clone();
1431 new_spec.image = Some(target.clone());
1432 new_spec
1433 };
1434
1435 if new_spec == host.spec {
1436 println!("Image specification is unchanged.");
1437 if opts.apply && host.status.staged.is_some() {
1438 crate::reboot::reboot()?;
1439 }
1440 return Ok(());
1441 }
1442
1443 const SWITCH_JOURNAL_ID: &str = "7a6b5c4d3e2f1a0b9c8d7e6f5a4b3c2d1";
1445 let old_image = host
1446 .spec
1447 .image
1448 .as_ref()
1449 .map(|i| i.image.as_str())
1450 .unwrap_or("none");
1451
1452 tracing::info!(
1453 message_id = SWITCH_JOURNAL_ID,
1454 bootc.old_image_reference = old_image,
1455 bootc.new_image_reference = &target.image,
1456 bootc.new_image_transport = &target.transport,
1457 "Switching from image {} to {}",
1458 old_image,
1459 target.image
1460 );
1461
1462 let new_spec = RequiredHostSpec::from_spec(&new_spec)?;
1463
1464 let use_unified = if opts.unified_storage_exp {
1468 true
1469 } else {
1470 crate::deploy::image_exists_in_unified_storage(storage, &target).await?
1471 };
1472
1473 let fetched = if use_unified {
1474 crate::deploy::pull_unified(
1475 repo,
1476 &target,
1477 None,
1478 opts.quiet,
1479 prog.clone(),
1480 storage,
1481 Some(&booted_ostree.deployment),
1482 )
1483 .await?
1484 } else {
1485 crate::deploy::pull(
1486 repo,
1487 &target,
1488 None,
1489 opts.quiet,
1490 prog.clone(),
1491 Some(&booted_ostree.deployment),
1492 )
1493 .await?
1494 };
1495
1496 if !opts.retain {
1497 if let Some(booted_origin) = booted_ostree.deployment.origin() {
1499 if let Some(ostree_ref) = booted_origin.optional_string("origin", "refspec")? {
1500 let (remote, ostree_ref) =
1501 ostree::parse_refspec(&ostree_ref).context("Failed to parse ostree ref")?;
1502 repo.set_ref_immediate(remote.as_deref(), &ostree_ref, None, cancellable)?;
1503 }
1504 }
1505 }
1506
1507 let stateroot = booted_ostree.stateroot();
1508 let from = MergeState::from_stateroot(storage, &stateroot)?;
1509 crate::deploy::stage(storage, from, &fetched, &new_spec, prog.clone(), false).await?;
1510
1511 storage.update_mtime()?;
1512
1513 if opts.soft_reboot.is_some() {
1514 let updated_host = crate::status::get_status(booted_ostree)?.1;
1517 handle_staged_soft_reboot(booted_ostree, opts.soft_reboot, &updated_host)?;
1518 }
1519
1520 if opts.apply {
1521 crate::reboot::reboot()?;
1522 }
1523
1524 Ok(())
1525}
1526
1527#[context("Switching")]
1529async fn switch(opts: SwitchOpts) -> Result<()> {
1530 if opts.mutate_in_place {
1534 let target = imgref_for_switch(&opts)?;
1535 let deployid = {
1536 let target = target.clone();
1538 let root = cap_std::fs::Dir::open_ambient_dir("/", cap_std::ambient_authority())?;
1539 tokio::task::spawn_blocking(move || {
1540 crate::deploy::switch_origin_inplace(&root, &target)
1541 })
1542 .await??
1543 };
1544 println!("Updated {deployid} to pull from {target}");
1545 return Ok(());
1546 }
1547 let storage = &get_storage().await?;
1548 match storage.kind()? {
1549 BootedStorageKind::Ostree(booted_ostree) => {
1550 switch_ostree(opts, storage, &booted_ostree).await
1551 }
1552 BootedStorageKind::Composefs(booted_cfs) => {
1553 switch_composefs(opts, storage, &booted_cfs).await
1554 }
1555 }
1556}
1557
1558#[context("Rollback (ostree)")]
1560async fn rollback_ostree(
1561 opts: &RollbackOpts,
1562 storage: &Storage,
1563 booted_ostree: &BootedOstree<'_>,
1564) -> Result<()> {
1565 crate::deploy::rollback(storage).await?;
1566
1567 if opts.soft_reboot.is_some() {
1568 let host = crate::status::get_status(booted_ostree)?.1;
1570
1571 handle_soft_reboot(
1572 opts.soft_reboot,
1573 host.status.rollback.as_ref(),
1574 "rollback",
1575 || soft_reboot_rollback(booted_ostree),
1576 )?;
1577 }
1578
1579 Ok(())
1580}
1581
1582#[context("Rollback")]
1584async fn rollback(opts: &RollbackOpts) -> Result<()> {
1585 let storage = &get_storage().await?;
1586 match storage.kind()? {
1587 BootedStorageKind::Ostree(booted_ostree) => {
1588 rollback_ostree(opts, storage, &booted_ostree).await
1589 }
1590 BootedStorageKind::Composefs(booted_cfs) => composefs_rollback(storage, &booted_cfs).await,
1591 }
1592}
1593
1594#[context("Editing spec (ostree)")]
1596async fn edit_ostree(
1597 opts: EditOpts,
1598 storage: &Storage,
1599 booted_ostree: &BootedOstree<'_>,
1600) -> Result<()> {
1601 let repo = &booted_ostree.repo();
1602 let (_, host) = crate::status::get_status(booted_ostree)?;
1603
1604 let new_host: Host = if let Some(filename) = opts.filename {
1605 let mut r = std::io::BufReader::new(std::fs::File::open(filename)?);
1606 serde_yaml::from_reader(&mut r)?
1607 } else {
1608 let tmpf = tempfile::NamedTempFile::with_suffix(".yaml")?;
1609 serde_yaml::to_writer(std::io::BufWriter::new(tmpf.as_file()), &host)?;
1610 crate::utils::spawn_editor(&tmpf)?;
1611 tmpf.as_file().seek(std::io::SeekFrom::Start(0))?;
1612 serde_yaml::from_reader(&mut tmpf.as_file())?
1613 };
1614
1615 if new_host.spec == host.spec {
1616 println!("Edit cancelled, no changes made.");
1617 return Ok(());
1618 }
1619 host.spec.verify_transition(&new_host.spec)?;
1620 let new_spec = RequiredHostSpec::from_spec(&new_host.spec)?;
1621
1622 let prog = ProgressWriter::default();
1623
1624 if host.spec.boot_order != new_host.spec.boot_order {
1627 return crate::deploy::rollback(storage).await;
1628 }
1629
1630 let fetched = crate::deploy::pull(
1631 repo,
1632 new_spec.image,
1633 None,
1634 opts.quiet,
1635 prog.clone(),
1636 Some(&booted_ostree.deployment),
1637 )
1638 .await?;
1639
1640 let stateroot = booted_ostree.stateroot();
1643 let from = MergeState::from_stateroot(storage, &stateroot)?;
1644 crate::deploy::stage(storage, from, &fetched, &new_spec, prog.clone(), false).await?;
1645
1646 storage.update_mtime()?;
1647
1648 Ok(())
1649}
1650
1651#[context("Editing spec")]
1653async fn edit(opts: EditOpts) -> Result<()> {
1654 let storage = &get_storage().await?;
1655 match storage.kind()? {
1656 BootedStorageKind::Ostree(booted_ostree) => {
1657 edit_ostree(opts, storage, &booted_ostree).await
1658 }
1659 BootedStorageKind::Composefs(_) => {
1660 anyhow::bail!("Edit is not yet supported for composefs backend")
1661 }
1662 }
1663}
1664
1665async fn usroverlay(access_mode: FilesystemOverlayAccessMode) -> Result<()> {
1667 let args = match access_mode {
1670 FilesystemOverlayAccessMode::ReadOnly => ["admin", "unlock", "--transient"].as_slice(),
1672
1673 FilesystemOverlayAccessMode::ReadWrite => ["admin", "unlock"].as_slice(),
1674 };
1675 Err(Command::new("ostree").args(args).exec().into())
1676}
1677
1678fn join_host_ipc_namespace() -> Result<()> {
1691 let caps = rustix::thread::capabilities(None).context("capget")?;
1692 if !caps
1693 .effective
1694 .contains(rustix::thread::CapabilitySet::SYS_ADMIN)
1695 {
1696 return Ok(());
1697 }
1698 let ns_pid1 = match std::fs::read_link("/proc/1/ns/ipc") {
1699 Ok(v) => v,
1700 Err(e) if e.kind() == std::io::ErrorKind::PermissionDenied => {
1701 return Ok(());
1702 }
1703 Err(e) => return Err(e).context("reading /proc/1/ns/ipc"),
1704 };
1705 let ns_self = std::fs::read_link("/proc/self/ns/ipc").context("reading /proc/self/ns/ipc")?;
1706 if ns_pid1 != ns_self {
1707 let pid1ipcns = std::fs::File::open("/proc/1/ns/ipc").context("open pid1 ipcns")?;
1708 rustix::thread::move_into_link_name_space(
1709 pid1ipcns.as_fd(),
1710 Some(rustix::thread::LinkNameSpaceType::InterProcessCommunication),
1711 )
1712 .context("setns(ipc)")?;
1713 }
1714 Ok(())
1715}
1716
1717#[allow(unsafe_code)]
1720pub fn global_init() -> Result<()> {
1721 join_host_ipc_namespace()?;
1722 ostree::glib::set_prgname(bootc_utils::NAME.into());
1725 if let Err(e) = rustix::thread::set_name(&CString::new(bootc_utils::NAME).unwrap()) {
1726 eprintln!("failed to set name: {e}");
1728 }
1729 ostree::SePolicy::set_null_log();
1731 let am_root = rustix::process::getuid().is_root();
1732 if std::env::var_os("HOME").is_none() && am_root {
1735 unsafe {
1740 std::env::set_var("HOME", "/root");
1741 }
1742 }
1743 Ok(())
1744}
1745
1746pub async fn run_from_iter<I>(args: I) -> Result<()>
1749where
1750 I: IntoIterator,
1751 I::Item: Into<OsString> + Clone,
1752{
1753 run_from_opt(Opt::parse_including_static(args)).await
1754}
1755
1756fn callname_from_argv0(argv0: &OsStr) -> &str {
1760 let default = "bootc";
1761 std::path::Path::new(argv0)
1762 .file_name()
1763 .and_then(|s| s.to_str())
1764 .filter(|s| !s.is_empty())
1765 .unwrap_or(default)
1766}
1767
1768impl Opt {
1769 fn parse_including_static<I>(args: I) -> Self
1772 where
1773 I: IntoIterator,
1774 I::Item: Into<OsString> + Clone,
1775 {
1776 let mut args = args.into_iter();
1777 let first = if let Some(first) = args.next() {
1778 let first: OsString = first.into();
1779 let argv0 = callname_from_argv0(&first);
1780 tracing::debug!("argv0={argv0:?}");
1781 let mapped = match argv0 {
1782 InternalsOpts::GENERATOR_BIN => {
1783 Some(["bootc", "internals", "systemd-generator"].as_slice())
1784 }
1785 "ostree-container" | "ostree-ima-sign" | "ostree-provisional-repair" => {
1786 Some(["bootc", "internals", "ostree-ext"].as_slice())
1787 }
1788 _ => None,
1789 };
1790 if let Some(base_args) = mapped {
1791 let base_args = base_args.iter().map(OsString::from);
1792 return Opt::parse_from(base_args.chain(args.map(|i| i.into())));
1793 }
1794 Some(first)
1795 } else {
1796 None
1797 };
1798 Opt::parse_from(first.into_iter().chain(args.map(|i| i.into())))
1799 }
1800}
1801
1802async fn run_from_opt(opt: Opt) -> Result<()> {
1804 let root = &Dir::open_ambient_dir("/", cap_std::ambient_authority())?;
1805 match opt {
1806 Opt::Upgrade(opts) => {
1807 let storage = &get_storage().await?;
1808 match storage.kind()? {
1809 BootedStorageKind::Ostree(booted_ostree) => {
1810 upgrade(opts, storage, &booted_ostree).await
1811 }
1812 BootedStorageKind::Composefs(booted_cfs) => {
1813 upgrade_composefs(opts, storage, &booted_cfs).await
1814 }
1815 }
1816 }
1817 Opt::Switch(opts) => switch(opts).await,
1818 Opt::Rollback(opts) => {
1819 rollback(&opts).await?;
1820 if opts.apply {
1821 crate::reboot::reboot()?;
1822 }
1823 Ok(())
1824 }
1825 Opt::Edit(opts) => edit(opts).await,
1826 Opt::UsrOverlay(opts) => {
1827 use crate::store::Environment;
1828 let env = Environment::detect()?;
1829 let access_mode = if opts.read_only {
1830 FilesystemOverlayAccessMode::ReadOnly
1831 } else {
1832 FilesystemOverlayAccessMode::ReadWrite
1833 };
1834 match env {
1835 Environment::OstreeBooted => usroverlay(access_mode).await,
1836 Environment::ComposefsBooted(_) => composefs_usr_overlay(access_mode),
1837 _ => anyhow::bail!("usroverlay only applies on booted hosts"),
1838 }
1839 }
1840 Opt::Container(opts) => match opts {
1841 ContainerOpts::Inspect {
1842 rootfs,
1843 json,
1844 format,
1845 } => crate::status::container_inspect(&rootfs, json, format),
1846 ContainerOpts::Lint {
1847 rootfs,
1848 fatal_warnings,
1849 list,
1850 skip,
1851 no_truncate,
1852 } => {
1853 if list {
1854 return lints::lint_list(std::io::stdout().lock());
1855 }
1856 let warnings = if fatal_warnings {
1857 lints::WarningDisposition::FatalWarnings
1858 } else {
1859 lints::WarningDisposition::AllowWarnings
1860 };
1861 let root_type = if rootfs == "/" {
1862 lints::RootType::Running
1863 } else {
1864 lints::RootType::Alternative
1865 };
1866
1867 let root = &Dir::open_ambient_dir(rootfs, cap_std::ambient_authority())?;
1868 let skip = skip.iter().map(|s| s.as_str());
1869 lints::lint(
1870 root,
1871 warnings,
1872 root_type,
1873 skip,
1874 std::io::stdout().lock(),
1875 no_truncate,
1876 )?;
1877 Ok(())
1878 }
1879 ContainerOpts::SplitKernelAndRootfs { rootfs, output } => {
1880 use crate::kernel::{KernelType, find_kernel};
1881
1882 let root = Dir::open_ambient_dir(&rootfs, ambient_authority())?;
1883
1884 let kernel_internal = find_kernel(&root)?
1885 .ok_or_else(|| anyhow::anyhow!("No kernel found in rootfs"))?;
1886
1887 if kernel_internal.kernel.unified {
1888 anyhow::bail!("UKIs are not supported");
1889 }
1890
1891 match &kernel_internal.k_type {
1892 KernelType::Vmlinuz { path, initramfs } => {
1893 let kver = &kernel_internal.kernel.version;
1894 let kernel_output_dir = output.join(kver);
1895 std::fs::create_dir_all(&kernel_output_dir)?;
1896
1897 let vmlinuz_src = rootfs.join(path);
1898 let initramfs_src = rootfs.join(initramfs);
1899 let vmlinuz_dst = kernel_output_dir.join("vmlinuz");
1900 let initramfs_dst = kernel_output_dir.join("initramfs.img");
1901
1902 std::fs::rename(&vmlinuz_src, &vmlinuz_dst).context("Moving vmlinuz")?;
1903 std::fs::rename(&initramfs_src, &initramfs_dst)
1904 .context("Moving initramfs")?;
1905 }
1906
1907 KernelType::Uki { .. } => {
1908 anyhow::bail!("UKIs are not supported");
1909 }
1910 }
1911
1912 Ok(())
1913 }
1914 ContainerOpts::ComputeComposefsDigest {
1915 path,
1916 write_dumpfile_to,
1917 } => {
1918 let digest = compute_composefs_digest(&path, write_dumpfile_to.as_deref()).await?;
1919 println!("{digest}");
1920 Ok(())
1921 }
1922 ContainerOpts::ComputeComposefsDigestFromStorage {
1923 write_dumpfile_to,
1924 image,
1925 } => {
1926 let (_td_guard, repo) = new_temp_composefs_repo()?;
1927
1928 let mut proxycfg = crate::deploy::new_proxy_config();
1929
1930 let image = if let Some(image) = image {
1931 image
1932 } else {
1933 let host_container_store = Utf8Path::new("/run/host-container-storage");
1934 let container_info = crate::containerenv::get_container_execution_info(&root)?;
1937 let iid = container_info.imageid;
1938 tracing::debug!("Computing digest of {iid}");
1939
1940 if !host_container_store.try_exists()? {
1941 anyhow::bail!(
1942 "Must be readonly mount of host container store: {host_container_store}"
1943 );
1944 }
1945 let mut cmd = Command::new(bootc_utils::skopeo_bin());
1947 set_additional_image_store(&mut cmd, "/run/host-container-storage");
1948 proxycfg.skopeo_cmd = Some(cmd);
1949 iid
1950 };
1951
1952 let imgref = format!("containers-storage:{image}");
1953 let host_store = std::path::Path::new("/run/host-container-storage");
1954 let opts = composefs_oci::PullOptions {
1955 img_proxy_config: Some(proxycfg),
1956 additional_image_stores: &[host_store],
1957 ..Default::default()
1958 };
1959 let pull_result = composefs_oci::pull(&repo, &imgref, None, opts)
1960 .await
1961 .context("Pulling image")?;
1962 let mut fs = composefs_oci::image::create_filesystem(
1963 &repo,
1964 &pull_result.config_digest,
1965 Some(&pull_result.config_verity),
1966 &Default::default(),
1967 )
1968 .context("Populating fs")?;
1969 fs.transform_for_boot(&repo).context("Preparing for boot")?;
1970 let id = fs.compute_image_id(repo.erofs_version());
1971 println!("{}", id.to_hex());
1972
1973 if let Some(path) = write_dumpfile_to.as_deref() {
1974 let mut w = File::create(path)
1975 .with_context(|| format!("Opening {path}"))
1976 .map(BufWriter::new)?;
1977 dumpfile::write_dumpfile(&mut w, &fs).context("Writing dumpfile")?;
1978 }
1979
1980 Ok(())
1981 }
1982 ContainerOpts::Ukify {
1983 rootfs,
1984 kargs,
1985 allow_missing_verity,
1986 write_dumpfile_to,
1987 kernel_dir,
1988 args,
1989 } => {
1990 let kernel = match kernel_dir {
1991 Some(kernel_dir) => {
1992 let kver = kernel_dir
1993 .components()
1994 .last()
1995 .ok_or_else(|| anyhow::anyhow!("Could not determine kernel version"))?;
1996
1997 Some(crate::kernel::KernelInternal {
1998 kernel: crate::kernel::Kernel {
1999 unified: false,
2000 version: kver.to_string(),
2001 },
2002 k_type: crate::kernel::KernelType::Vmlinuz {
2003 path: kernel_dir.join("vmlinuz"),
2004 initramfs: kernel_dir.join("initramfs.img"),
2005 },
2006 })
2007 }
2008
2009 None => None,
2010 };
2011
2012 crate::ukify::build_ukify(
2013 &rootfs,
2014 &kargs,
2015 &args,
2016 kernel,
2017 allow_missing_verity,
2018 write_dumpfile_to.as_deref(),
2019 )
2020 .await
2021 }
2022 ContainerOpts::Export {
2023 format,
2024 target,
2025 output,
2026 kernel_in_boot,
2027 disable_selinux,
2028 } => {
2029 crate::container_export::export(
2030 &format,
2031 &target,
2032 output.as_deref(),
2033 kernel_in_boot,
2034 disable_selinux,
2035 )
2036 .await
2037 }
2038 },
2039 Opt::Completion { shell } => {
2040 use clap_complete::aot::generate;
2041
2042 let mut cmd = Opt::command();
2043 let mut stdout = std::io::stdout();
2044 let bin_name = "bootc";
2045 generate(shell, &mut cmd, bin_name, &mut stdout);
2046 Ok(())
2047 }
2048 Opt::Image(opts) => match opts {
2049 ImageOpts::List {
2050 list_type,
2051 list_format,
2052 } => crate::image::list_entrypoint(list_type, list_format).await,
2053
2054 ImageOpts::CopyToStorage { source, target } => {
2055 let host = get_host().await?;
2057
2058 let storage = get_storage().await?;
2059
2060 match storage.kind()? {
2061 BootedStorageKind::Ostree(..) => {
2062 crate::image::push_entrypoint(
2063 &storage,
2064 &host,
2065 source.as_deref(),
2066 target.as_deref(),
2067 )
2068 .await
2069 }
2070 BootedStorageKind::Composefs(booted) => {
2071 bootc_composefs::export::export_repo_to_image(
2072 &storage,
2073 &booted,
2074 source.as_deref(),
2075 target.as_deref(),
2076 )
2077 .await
2078 }
2079 }
2080 }
2081 ImageOpts::SetUnified => crate::image::set_unified_entrypoint().await,
2082 ImageOpts::PullFromDefaultStorage { image } => {
2083 let storage = get_storage().await?;
2084 storage
2085 .get_ensure_imgstore()?
2086 .pull_from_host_storage(&image)
2087 .await
2088 }
2089 ImageOpts::Cmd(opt) => {
2090 let storage = get_storage().await?;
2091 let imgstore = storage.get_ensure_imgstore()?;
2092 match opt {
2093 ImageCmdOpts::List { args } => {
2094 crate::image::imgcmd_entrypoint(imgstore, "list", &args).await
2095 }
2096 ImageCmdOpts::Build { args } => {
2097 crate::image::imgcmd_entrypoint(imgstore, "build", &args).await
2098 }
2099 ImageCmdOpts::Pull { images } => {
2100 for image in &images {
2101 imgstore.pull_with_progress(image).await?;
2102 }
2103 Ok(())
2104 }
2105 ImageCmdOpts::Push { args } => {
2106 crate::image::imgcmd_entrypoint(imgstore, "push", &args).await
2107 }
2108 }
2109 }
2110 },
2111 Opt::Install(opts) => match opts {
2112 #[cfg(feature = "install-to-disk")]
2113 InstallOpts::ToDisk(opts) => crate::install::install_to_disk(opts).await,
2114 InstallOpts::ToFilesystem(opts) => {
2115 crate::install::install_to_filesystem(opts, false, crate::install::Cleanup::Skip)
2116 .await
2117 }
2118 InstallOpts::ToExistingRoot(opts) => {
2119 crate::install::install_to_existing_root(opts).await
2120 }
2121 InstallOpts::Reset(opts) => crate::install::install_reset(opts).await,
2122 InstallOpts::PrintConfiguration(opts) => crate::install::print_configuration(opts),
2123 InstallOpts::EnsureCompletion {} => {
2124 let rootfs = &Dir::open_ambient_dir("/", cap_std::ambient_authority())?;
2125 crate::install::completion::run_from_anaconda(rootfs).await
2126 }
2127 InstallOpts::Finalize { root_path } => {
2128 crate::install::install_finalize(&root_path).await
2129 }
2130 },
2131 Opt::LoaderEntries(opts) => match opts {
2132 LoaderEntriesOpts::SetOptionsForSource(opts) => {
2133 let storage = get_storage().await?;
2134 let sysroot = storage.get_ostree()?;
2135 crate::loader_entries::set_options_for_source_staged(
2136 sysroot,
2137 &opts.source,
2138 opts.options.as_deref(),
2139 )?;
2140 Ok(())
2141 }
2142 },
2143 Opt::ExecInHostMountNamespace { args } => {
2144 crate::install::exec_in_host_mountns(args.as_slice())
2145 }
2146 Opt::Status(opts) => super::status::status(opts).await,
2147 Opt::Internals(opts) => match opts {
2148 InternalsOpts::SystemdGenerator {
2149 normal_dir,
2150 early_dir: _,
2151 late_dir: _,
2152 } => {
2153 let unit_dir = &Dir::open_ambient_dir(normal_dir, cap_std::ambient_authority())?;
2154 crate::generator::generator(root, unit_dir)
2155 }
2156 InternalsOpts::OstreeExt { args } => {
2157 ostree_ext::cli::run_from_iter(["ostree-ext".into()].into_iter().chain(args)).await
2158 }
2159 InternalsOpts::OstreeContainer { args } => {
2160 ostree_ext::cli::run_from_iter(
2161 ["ostree-ext".into(), "container".into()]
2162 .into_iter()
2163 .chain(args),
2164 )
2165 .await
2166 }
2167 InternalsOpts::TestComposefs => {
2168 let storage = get_storage().await?;
2170 let cfs = storage.get_ensure_composefs()?;
2171 let testdata = b"some test data";
2172 let testdata_digest = hex::encode(openssl::sha::sha256(testdata));
2173 let mut w = cfs.create_stream(0)?;
2174 w.write_inline(testdata);
2175 let object = cfs
2176 .write_stream(w, &testdata_digest, Some("testobject"))?
2177 .to_hex();
2178 assert_eq!(
2179 object,
2180 "84245c6936db9939dda9c1fbeafdcbd2b49f7605354c88d4f016c4d941551f45bad0fbcdbee12ba8adfe4fb63541de57ac02729edbacdb556325e342b89d340d"
2181 );
2182 Ok(())
2183 }
2184 InternalsOpts::Fsverity(args) => match args {
2186 FsverityOpts::Measure { path } => {
2187 let fd =
2188 std::fs::File::open(&path).with_context(|| format!("Reading {path}"))?;
2189 let digest: fsverity::Sha256HashValue = fsverity::measure_verity(&fd)?;
2190 let digest = digest.to_hex();
2191 println!("{digest}");
2192 Ok(())
2193 }
2194 FsverityOpts::Enable { path } => {
2195 let fd =
2196 std::fs::File::open(&path).with_context(|| format!("Reading {path}"))?;
2197 fsverity::enable_verity_raw::<fsverity::Sha256HashValue>(&fd)?;
2198 Ok(())
2199 }
2200 },
2201 InternalsOpts::Cfs { args } => composefs_ctl::run_from_iter(args.iter()).await,
2202 InternalsOpts::Reboot => crate::reboot::reboot(),
2203 InternalsOpts::Fsck => {
2204 let storage = &get_storage().await?;
2205 crate::fsck::fsck(&storage, std::io::stdout().lock()).await?;
2206 Ok(())
2207 }
2208 InternalsOpts::FixupEtcFstab => crate::deploy::fixup_etc_fstab(&root),
2209 InternalsOpts::SysusersSync => crate::sysusers_cleanup::run(&root),
2210 InternalsOpts::PrintJsonSchema { of } => {
2211 let schema = match of {
2212 SchemaType::Host => schema_for!(crate::spec::Host),
2213 SchemaType::Progress => schema_for!(crate::progress_jsonl::Event),
2214 };
2215 let mut stdout = std::io::stdout().lock();
2216 serde_json::to_writer_pretty(&mut stdout, &schema)?;
2217 Ok(())
2218 }
2219 InternalsOpts::Cleanup => {
2220 let storage = get_storage().await?;
2221 crate::deploy::cleanup(&storage).await
2222 }
2223 InternalsOpts::Relabel { as_path, path } => {
2224 let root = &Dir::open_ambient_dir("/", cap_std::ambient_authority())?;
2225 let path = path.strip_prefix("/")?;
2226 let sepolicy =
2227 &ostree::SePolicy::new(&gio::File::for_path("/"), gio::Cancellable::NONE)?;
2228 crate::lsm::relabel_recurse(root, path, as_path.as_deref(), sepolicy)?;
2229 Ok(())
2230 }
2231 InternalsOpts::RelabelOverlayMountpoints => {
2232 crate::generator::relabel_overlay_mountpoints()
2233 }
2234 InternalsOpts::BootcInstallCompletion { sysroot, stateroot } => {
2235 let rootfs = &Dir::open_ambient_dir("/", cap_std::ambient_authority())?;
2236 crate::install::completion::run_from_ostree(rootfs, &sysroot, &stateroot).await
2237 }
2238 InternalsOpts::LoopbackCleanupHelper { device } => {
2239 crate::blockdev::run_loopback_cleanup_helper(&device).await
2240 }
2241 InternalsOpts::AllocateCleanupLoopback { file_path: _ } => {
2242 let temp_file =
2244 tempfile::NamedTempFile::new().context("Failed to create temporary file")?;
2245 let temp_path = temp_file.path();
2246
2247 let loopback = crate::blockdev::LoopbackDevice::new(temp_path)
2249 .context("Failed to create loopback device")?;
2250
2251 println!("Created loopback device: {}", loopback.path());
2252
2253 loopback
2255 .close()
2256 .context("Failed to close loopback device")?;
2257
2258 println!("Successfully closed loopback device");
2259 Ok(())
2260 }
2261 #[cfg(feature = "rhsm")]
2262 InternalsOpts::PublishRhsmFacts => crate::rhsm::publish_facts(&root).await,
2263 #[cfg(feature = "docgen")]
2264 InternalsOpts::DumpCliJson => {
2265 use clap::CommandFactory;
2266 let cmd = Opt::command();
2267 let json = crate::cli_json::dump_cli_json(&cmd)?;
2268 println!("{}", json);
2269 Ok(())
2270 }
2271 InternalsOpts::DirDiff {
2272 pristine_etc,
2273 current_etc,
2274 new_etc,
2275 merge,
2276 } => {
2277 let pristine_etc =
2278 Dir::open_ambient_dir(pristine_etc, cap_std::ambient_authority())?;
2279 let current_etc = Dir::open_ambient_dir(current_etc, cap_std::ambient_authority())?;
2280 let new_etc = Dir::open_ambient_dir(new_etc, cap_std::ambient_authority())?;
2281
2282 let (p, c, n) =
2283 etc_merge::traverse_etc(&pristine_etc, ¤t_etc, Some(&new_etc))?;
2284
2285 let n = n
2286 .as_ref()
2287 .ok_or_else(|| anyhow::anyhow!("Failed to get new directory tree"))?;
2288
2289 let diff = compute_diff(&p, &c, &n)?;
2290 print_diff(&diff, &mut std::io::stdout());
2291
2292 if merge {
2293 etc_merge::merge(¤t_etc, &c, &new_etc, &n, &diff)?;
2294 }
2295
2296 Ok(())
2297 }
2298 InternalsOpts::PrepSoftReboot {
2299 deployment,
2300 reboot,
2301 reset,
2302 } => {
2303 let storage = &get_storage().await?;
2304
2305 match storage.kind()? {
2306 BootedStorageKind::Ostree(..) => {
2307 anyhow::bail!("soft-reboot only implemented for composefs")
2309 }
2310
2311 BootedStorageKind::Composefs(booted_cfs) => {
2312 if reset {
2313 return reset_soft_reboot();
2314 }
2315
2316 prepare_soft_reboot_composefs(
2317 &storage,
2318 &booted_cfs,
2319 deployment.as_deref(),
2320 SoftRebootMode::Required,
2321 reboot,
2322 )
2323 .await
2324 }
2325 }
2326 }
2327 InternalsOpts::ComposefsGC {
2328 dry_run,
2329 assert_no_op,
2330 prune_repo,
2331 } => {
2332 let storage = &get_storage().await?;
2333
2334 match storage.kind()? {
2335 BootedStorageKind::Ostree(..) => {
2336 anyhow::bail!("composefs-gc only works for composefs backend");
2337 }
2338
2339 BootedStorageKind::Composefs(booted_cfs) => {
2340 let dry_run = dry_run || assert_no_op;
2341 let gc_result = composefs_gc(
2342 storage,
2343 &booted_cfs,
2344 GCOpts {
2345 dry_run,
2346 prune_repo,
2347 },
2348 )
2349 .await?;
2350
2351 if dry_run {
2352 println!("Dry run (no files deleted)");
2353 }
2354
2355 println!(
2356 "Objects: {} removed ({} bytes)",
2357 gc_result.objects_removed, gc_result.objects_bytes
2358 );
2359
2360 if gc_result.images_pruned > 0 || gc_result.streams_pruned > 0 {
2361 println!(
2362 "Pruned symlinks: {} images, {} streams",
2363 gc_result.images_pruned, gc_result.streams_pruned
2364 );
2365 }
2366
2367 if assert_no_op {
2368 let is_noop = gc_result.objects_removed == 0
2369 && gc_result.images_pruned == 0
2370 && gc_result.streams_pruned == 0;
2371 if !is_noop {
2372 anyhow::bail!(
2373 "--assert-no-op: GC would remove {} object(s), {} image symlink(s), {} stream symlink(s) (issue #1808)",
2374 gc_result.objects_removed,
2375 gc_result.images_pruned,
2376 gc_result.streams_pruned,
2377 );
2378 }
2379 }
2380
2381 Ok(())
2382 }
2383 }
2384 }
2385 InternalsOpts::Blockdev(opts) => {
2386 let dev = match opts {
2387 BlockdevOpts::Ls { device } => crate::blockdev::list_dev(&device)?,
2388 BlockdevOpts::LsFilesystem { path } => {
2389 let dir = Dir::open_ambient_dir(&path, cap_std::ambient_authority())?;
2390 crate::blockdev::list_dev_by_dir(&dir)?
2391 }
2392 };
2393 serde_json::to_writer_pretty(std::io::stdout().lock(), &dev)?;
2394 println!();
2395 Ok(())
2396 }
2397 InternalsOpts::Uki(uki_opts) => match uki_opts {
2398 UkiSubcommands::Extract { path, output_path } => {
2399 let mut uki_file =
2400 std::fs::File::open(&path).with_context(|| format!("Opening {path}"))?;
2401
2402 let uname =
2403 composefs_boot::uki::get_text_section_buffered(&mut uki_file, ".uname")
2404 .context("Getting uname")?;
2405
2406 std::fs::create_dir_all(&output_path).context("Creating output directory")?;
2407
2408 let output_dir = Dir::open_ambient_dir(&output_path, ambient_authority())
2409 .context("Opening output dir")?;
2410 output_dir.create_dir(&uname)?;
2411
2412 let output_dir = output_dir.open_dir(&uname)?;
2413
2414 for (section_name, file_name) in
2415 [(".linux", "vmlinuz"), (".initrd", "initramfs.img")]
2416 {
2417 uki_file
2418 .seek(SeekFrom::Start(0))
2419 .context("Seeking to start")?;
2420 let section =
2421 composefs_boot::uki::get_section_buffered(&mut uki_file, section_name)
2422 .with_context(|| format!("Getting {section_name} section"))?;
2423 output_dir
2424 .write(file_name, section)
2425 .with_context(|| format!("Writing {file_name}"))?;
2426 }
2427
2428 Ok(())
2429 }
2430 },
2431 },
2432 Opt::State(opts) => match opts {
2433 StateOpts::WipeOstree => {
2434 let sysroot = ostree::Sysroot::new_default();
2435 sysroot.load(gio::Cancellable::NONE)?;
2436 crate::deploy::wipe_ostree(sysroot).await?;
2437 Ok(())
2438 }
2439 },
2440
2441 Opt::ComposefsFinalizeStaged => {
2442 let storage = &get_storage().await?;
2443 match storage.kind()? {
2444 BootedStorageKind::Ostree(_) => {
2445 anyhow::bail!("ComposefsFinalizeStaged is only supported for composefs backend")
2446 }
2447 BootedStorageKind::Composefs(booted_cfs) => {
2448 composefs_backend_finalize(storage, &booted_cfs).await
2449 }
2450 }
2451 }
2452
2453 Opt::ConfigDiff => {
2454 let storage = &get_storage().await?;
2455 match storage.kind()? {
2456 BootedStorageKind::Ostree(_) => {
2457 anyhow::bail!("ConfigDiff is only supported for composefs backend")
2458 }
2459 BootedStorageKind::Composefs(booted_cfs) => {
2460 let diff = get_etc_diff(storage, &booted_cfs, None).await?;
2461 print_diff(&diff, &mut std::io::stdout());
2462 Ok(())
2463 }
2464 }
2465 }
2466
2467 Opt::DeleteDeployment { depl_id } => {
2468 let storage = &get_storage().await?;
2469 match storage.kind()? {
2470 BootedStorageKind::Ostree(_) => {
2471 anyhow::bail!("DeleteDeployment is only supported for composefs backend")
2472 }
2473 BootedStorageKind::Composefs(booted_cfs) => {
2474 delete_composefs_deployment(&depl_id, storage, &booted_cfs).await
2475 }
2476 }
2477 }
2478 }
2479}
2480
2481#[cfg(test)]
2482mod tests {
2483 use super::*;
2484
2485 #[test]
2486 fn test_callname() {
2487 use std::os::unix::ffi::OsStrExt;
2488
2489 let mapped_cases = [
2491 ("", "bootc"),
2492 ("/foo/bar", "bar"),
2493 ("/foo/bar/", "bar"),
2494 ("foo/bar", "bar"),
2495 ("../foo/bar", "bar"),
2496 ("usr/bin/ostree-container", "ostree-container"),
2497 ];
2498 for (input, output) in mapped_cases {
2499 assert_eq!(
2500 output,
2501 callname_from_argv0(OsStr::new(input)),
2502 "Handling mapped case {input}"
2503 );
2504 }
2505
2506 assert_eq!("bootc", callname_from_argv0(OsStr::from_bytes(b"foo\x80")));
2508
2509 let ident_cases = ["foo", "bootc"];
2511 for case in ident_cases {
2512 assert_eq!(
2513 case,
2514 callname_from_argv0(OsStr::new(case)),
2515 "Handling ident case {case}"
2516 );
2517 }
2518 }
2519
2520 #[test]
2521 fn test_parse_install_args() {
2522 let o = Opt::try_parse_from([
2524 "bootc",
2525 "install",
2526 "to-filesystem",
2527 "--target-no-signature-verification",
2528 "/target",
2529 ])
2530 .unwrap();
2531 let o = match o {
2532 Opt::Install(InstallOpts::ToFilesystem(fsopts)) => fsopts,
2533 o => panic!("Expected filesystem opts, not {o:?}"),
2534 };
2535 assert!(o.target_opts.target_no_signature_verification);
2536 assert_eq!(o.filesystem_opts.root_path.as_str(), "/target");
2537 assert_eq!(
2539 o.config_opts.bound_images,
2540 crate::install::BoundImagesOpt::Stored
2541 );
2542 }
2543
2544 #[test]
2545 fn test_parse_opts() {
2546 assert!(matches!(
2547 Opt::parse_including_static(["bootc", "status"]),
2548 Opt::Status(StatusOpts {
2549 json: false,
2550 format: None,
2551 format_version: None,
2552 booted: false,
2553 verbose: false
2554 })
2555 ));
2556 assert!(matches!(
2557 Opt::parse_including_static(["bootc", "status", "--format-version=0"]),
2558 Opt::Status(StatusOpts {
2559 format_version: Some(0),
2560 ..
2561 })
2562 ));
2563
2564 assert!(matches!(
2566 Opt::parse_including_static(["bootc", "status", "--verbose"]),
2567 Opt::Status(StatusOpts { verbose: true, .. })
2568 ));
2569
2570 assert!(matches!(
2572 Opt::parse_including_static(["bootc", "status", "-v"]),
2573 Opt::Status(StatusOpts { verbose: true, .. })
2574 ));
2575 }
2576
2577 #[test]
2578 fn test_parse_generator() {
2579 assert!(matches!(
2580 Opt::parse_including_static([
2581 "/usr/lib/systemd/system/bootc-systemd-generator",
2582 "/run/systemd/system"
2583 ]),
2584 Opt::Internals(InternalsOpts::SystemdGenerator { normal_dir, .. }) if normal_dir == "/run/systemd/system"
2585 ));
2586 }
2587
2588 #[test]
2589 fn test_parse_ostree_ext() {
2590 assert!(matches!(
2591 Opt::parse_including_static(["bootc", "internals", "ostree-container"]),
2592 Opt::Internals(InternalsOpts::OstreeContainer { .. })
2593 ));
2594
2595 fn peel(o: Opt) -> Vec<OsString> {
2596 match o {
2597 Opt::Internals(InternalsOpts::OstreeExt { args }) => args,
2598 o => panic!("unexpected {o:?}"),
2599 }
2600 }
2601 let args = peel(Opt::parse_including_static([
2602 "/usr/libexec/libostree/ext/ostree-ima-sign",
2603 "ima-sign",
2604 "--repo=foo",
2605 "foo",
2606 "bar",
2607 "baz",
2608 ]));
2609 assert_eq!(
2610 args.as_slice(),
2611 ["ima-sign", "--repo=foo", "foo", "bar", "baz"]
2612 );
2613
2614 let args = peel(Opt::parse_including_static([
2615 "/usr/libexec/libostree/ext/ostree-container",
2616 "container",
2617 "image",
2618 "pull",
2619 ]));
2620 assert_eq!(args.as_slice(), ["container", "image", "pull"]);
2621 }
2622
2623 #[test]
2624 fn test_parse_upgrade_options() {
2625 let o = Opt::try_parse_from(["bootc", "upgrade", "--tag", "v1.1"]).unwrap();
2627 match o {
2628 Opt::Upgrade(opts) => {
2629 assert_eq!(opts.tag, Some("v1.1".to_string()));
2630 }
2631 _ => panic!("Expected Upgrade variant"),
2632 }
2633
2634 let o = Opt::try_parse_from(["bootc", "upgrade", "--tag", "v1.1", "--check"]).unwrap();
2636 match o {
2637 Opt::Upgrade(opts) => {
2638 assert_eq!(opts.tag, Some("v1.1".to_string()));
2639 assert!(opts.check);
2640 }
2641 _ => panic!("Expected Upgrade variant"),
2642 }
2643 }
2644
2645 #[test]
2646 fn test_image_reference_with_tag() {
2647 let current = ImageReference {
2649 image: "quay.io/example/myapp:v1.0".to_string(),
2650 transport: "registry".to_string(),
2651 signature: None,
2652 };
2653 let result = current.with_tag("v1.1").unwrap();
2654 assert_eq!(result.image, "quay.io/example/myapp:v1.1");
2655 assert_eq!(result.transport, "registry");
2656
2657 let current_with_digest = ImageReference {
2659 image: "quay.io/example/myapp:v1.0@sha256:abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890".to_string(),
2660 transport: "registry".to_string(),
2661 signature: None,
2662 };
2663 let result = current_with_digest.with_tag("v2.0").unwrap();
2664 assert_eq!(result.image, "quay.io/example/myapp:v2.0");
2665
2666 let containers_storage = ImageReference {
2668 image: "localhost/myapp:v1.0".to_string(),
2669 transport: "containers-storage".to_string(),
2670 signature: None,
2671 };
2672 let result = containers_storage.with_tag("v1.1").unwrap();
2673 assert_eq!(result.image, "localhost/myapp:v1.1");
2674 assert_eq!(result.transport, "containers-storage");
2675
2676 let containers_storage_with_digest = ImageReference {
2678 image:
2679 "localhost/myapp:v1.0@sha256:abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890"
2680 .to_string(),
2681 transport: "containers-storage".to_string(),
2682 signature: None,
2683 };
2684 let result = containers_storage_with_digest.with_tag("v2.0").unwrap();
2685 assert_eq!(result.image, "localhost/myapp:v2.0");
2686 assert_eq!(result.transport, "containers-storage");
2687
2688 let no_tag = ImageReference {
2690 image: "localhost/myapp".to_string(),
2691 transport: "containers-storage".to_string(),
2692 signature: None,
2693 };
2694 let result = no_tag.with_tag("v1.0").unwrap();
2695 assert_eq!(result.image, "localhost/myapp:v1.0");
2696 assert_eq!(result.transport, "containers-storage");
2697 }
2698
2699 #[test]
2700 fn test_generate_completion_scripts_contain_commands() {
2701 use clap_complete::aot::{Shell, generate};
2702
2703 let want = ["install", "upgrade"];
2712
2713 for shell in [Shell::Bash, Shell::Zsh, Shell::Fish] {
2714 let mut cmd = Opt::command();
2715 let mut buf = Vec::new();
2716 generate(shell, &mut cmd, "bootc", &mut buf);
2717 let s = String::from_utf8(buf).expect("completion should be utf8");
2718 for w in &want {
2719 assert!(s.contains(w), "{shell:?} completion missing {w}");
2720 }
2721 }
2722 }
2723}