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)
1034 .await?
1035 .ok_or_else(|| anyhow!("System not booted via bootc"))?;
1036 Ok(r)
1037}
1038
1039#[context("Querying root privilege")]
1040pub(crate) fn require_root(is_container: bool) -> Result<()> {
1041 ensure!(
1042 rustix::process::getuid().is_root(),
1043 if is_container {
1044 "The user inside the container from which you are running this command must be root"
1045 } else {
1046 "This command must be executed as the root user"
1047 }
1048 );
1049
1050 ensure!(
1051 rustix::thread::capability_is_in_bounding_set(rustix::thread::CapabilitySet::SYS_ADMIN)?,
1052 if is_container {
1053 "The container must be executed with full privileges (e.g. --privileged flag)"
1054 } else {
1055 "This command requires full root privileges (CAP_SYS_ADMIN)"
1056 }
1057 );
1058
1059 tracing::trace!("Verified uid 0 with CAP_SYS_ADMIN");
1060
1061 Ok(())
1062}
1063
1064fn has_soft_reboot_capability(deployment: Option<&crate::spec::BootEntry>) -> bool {
1066 deployment.map(|d| d.soft_reboot_capable).unwrap_or(false)
1067}
1068
1069#[context("Preparing soft reboot")]
1071fn prepare_soft_reboot(sysroot: &SysrootLock, deployment: &ostree::Deployment) -> Result<()> {
1072 let cancellable = ostree::gio::Cancellable::NONE;
1073 sysroot
1074 .deployment_set_soft_reboot(deployment, false, cancellable)
1075 .context("Failed to prepare soft-reboot")?;
1076 Ok(())
1077}
1078
1079#[context("Handling soft reboot")]
1081fn handle_soft_reboot<F>(
1082 soft_reboot_mode: Option<SoftRebootMode>,
1083 entry: Option<&crate::spec::BootEntry>,
1084 deployment_type: &str,
1085 execute_soft_reboot: F,
1086) -> Result<()>
1087where
1088 F: FnOnce() -> Result<()>,
1089{
1090 let Some(mode) = soft_reboot_mode else {
1091 return Ok(());
1092 };
1093
1094 let can_soft_reboot = has_soft_reboot_capability(entry);
1095 match mode {
1096 SoftRebootMode::Required => {
1097 if can_soft_reboot {
1098 execute_soft_reboot()?;
1099 } else {
1100 anyhow::bail!(
1101 "Soft reboot was required but {} deployment is not soft-reboot capable",
1102 deployment_type
1103 );
1104 }
1105 }
1106 SoftRebootMode::Auto => {
1107 if can_soft_reboot {
1108 execute_soft_reboot()?;
1109 }
1110 }
1111 }
1112 Ok(())
1113}
1114
1115#[context("Handling staged soft reboot")]
1117fn handle_staged_soft_reboot(
1118 booted_ostree: &BootedOstree<'_>,
1119 soft_reboot_mode: Option<SoftRebootMode>,
1120 host: &crate::spec::Host,
1121) -> Result<()> {
1122 handle_soft_reboot(
1123 soft_reboot_mode,
1124 host.status.staged.as_ref(),
1125 "staged",
1126 || soft_reboot_staged(booted_ostree.sysroot),
1127 )
1128}
1129
1130#[context("Soft reboot staged deployment")]
1132fn soft_reboot_staged(sysroot: &SysrootLock) -> Result<()> {
1133 println!("Staged deployment is soft-reboot capable, preparing for soft-reboot...");
1134
1135 let deployments_list = sysroot.deployments();
1136 let staged_deployment = deployments_list
1137 .iter()
1138 .find(|d| d.is_staged())
1139 .ok_or_else(|| anyhow::anyhow!("Failed to find staged deployment"))?;
1140
1141 prepare_soft_reboot(sysroot, staged_deployment)?;
1142 Ok(())
1143}
1144
1145#[context("Soft reboot rollback deployment")]
1147fn soft_reboot_rollback(booted_ostree: &BootedOstree<'_>) -> Result<()> {
1148 println!("Rollback deployment is soft-reboot capable, preparing for soft-reboot...");
1149
1150 let deployments_list = booted_ostree.sysroot.deployments();
1151 let target_deployment = deployments_list
1152 .first()
1153 .ok_or_else(|| anyhow::anyhow!("No rollback deployment found!"))?;
1154
1155 prepare_soft_reboot(booted_ostree.sysroot, target_deployment)
1156}
1157
1158#[context("Preparing for write")]
1162pub(crate) fn prepare_for_write() -> Result<()> {
1163 use std::sync::atomic::{AtomicBool, Ordering};
1164
1165 static ENTERED: AtomicBool = AtomicBool::new(false);
1171 if ENTERED.load(Ordering::SeqCst) {
1172 return Ok(());
1173 }
1174 if ostree_ext::container_utils::running_in_container() {
1175 anyhow::bail!("Detected container; this command requires a booted host system.");
1176 }
1177 crate::cli::require_root(false)?;
1178 ensure_self_unshared_mount_namespace()?;
1179 if crate::lsm::selinux_enabled()? && !crate::lsm::selinux_ensure_install()? {
1180 tracing::debug!("Do not have install_t capabilities");
1181 }
1182 ENTERED.store(true, Ordering::SeqCst);
1183 Ok(())
1184}
1185
1186#[context("Upgrading")]
1188async fn upgrade(
1189 opts: UpgradeOpts,
1190 storage: &Storage,
1191 booted_ostree: &BootedOstree<'_>,
1192) -> Result<()> {
1193 let repo = &booted_ostree.repo();
1194
1195 let host = crate::status::get_status(booted_ostree)?.1;
1196 let current_image = host.spec.image.as_ref();
1197
1198 let derived_image = if let Some(ref tag) = opts.tag {
1200 let image = current_image.ok_or_else(|| {
1201 anyhow::anyhow!("--tag requires a booted image with a specified source")
1202 })?;
1203 Some(image.with_tag(tag)?)
1204 } else {
1205 None
1206 };
1207
1208 let imgref = derived_image.as_ref().or(current_image);
1209 let prog: ProgressWriter = opts.progress.try_into()?;
1210
1211 if imgref.is_none() {
1213 let booted_incompatible = host.status.booted.as_ref().is_some_and(|b| b.incompatible);
1214
1215 let staged_incompatible = host.status.staged.as_ref().is_some_and(|b| b.incompatible);
1216
1217 if booted_incompatible || staged_incompatible {
1218 return Err(anyhow::anyhow!(
1219 "Deployment contains local rpm-ostree modifications; cannot upgrade via bootc. You can run `rpm-ostree reset` to undo the modifications."
1220 ));
1221 }
1222 }
1223
1224 let imgref = imgref.ok_or_else(|| anyhow::anyhow!("No image source specified"))?;
1225 let spec = RequiredHostSpec { image: imgref };
1227 let booted_image = host
1228 .status
1229 .booted
1230 .as_ref()
1231 .map(|b| b.query_image(repo))
1232 .transpose()?
1233 .flatten();
1234 let staged = host.status.staged.as_ref();
1236 let staged_image = staged.as_ref().and_then(|s| s.image.as_ref());
1237 let mut changed = false;
1238
1239 if opts.from_downloaded {
1241 let ostree = storage.get_ostree()?;
1242 let staged_deployment = ostree
1243 .staged_deployment()
1244 .ok_or_else(|| anyhow::anyhow!("No staged deployment found"))?;
1245
1246 if staged_deployment.is_finalization_locked() {
1247 ostree.change_finalization(&staged_deployment)?;
1248 println!("Staged deployment will now be applied on reboot");
1249 } else {
1250 println!("Staged deployment is already set to apply on reboot");
1251 }
1252
1253 handle_staged_soft_reboot(booted_ostree, opts.soft_reboot, &host)?;
1254 if opts.apply {
1255 crate::reboot::reboot()?;
1256 }
1257 return Ok(());
1258 }
1259
1260 let use_unified = crate::deploy::image_exists_in_unified_storage(storage, imgref).await?;
1264
1265 if opts.check {
1266 let ostree_imgref = imgref.clone().into();
1267 let mut imp =
1268 crate::deploy::new_importer(repo, &ostree_imgref, Some(&booted_ostree.deployment))
1269 .await?;
1270 match imp.prepare().await? {
1271 PrepareResult::AlreadyPresent(_) => {
1272 println!("No changes in: {ostree_imgref:#}");
1273 }
1274 PrepareResult::Ready(r) => {
1275 crate::deploy::check_bootc_label(&r.config);
1276 println!("Update available for: {ostree_imgref:#}");
1277 if let Some(version) = r.version() {
1278 println!(" Version: {version}");
1279 }
1280 println!(" Digest: {}", r.manifest_digest);
1281 changed = true;
1282 if let Some(previous_image) = booted_image.as_ref() {
1283 let diff =
1284 ostree_container::ManifestDiff::new(&previous_image.manifest, &r.manifest);
1285 diff.print();
1286 }
1287 }
1288 }
1289 } else {
1290 let fetched = if use_unified {
1291 crate::deploy::pull_unified(
1292 repo,
1293 imgref,
1294 None,
1295 opts.quiet,
1296 prog.clone(),
1297 storage,
1298 Some(&booted_ostree.deployment),
1299 )
1300 .await?
1301 } else {
1302 crate::deploy::pull(
1303 repo,
1304 imgref,
1305 None,
1306 opts.quiet,
1307 prog.clone(),
1308 Some(&booted_ostree.deployment),
1309 )
1310 .await?
1311 };
1312 let staged_digest = staged_image.map(|s| s.digest().expect("valid digest in status"));
1313 let fetched_digest = &fetched.manifest_digest;
1314 tracing::debug!("staged: {staged_digest:?}");
1315 tracing::debug!("fetched: {fetched_digest}");
1316 let staged_unchanged = staged_digest
1317 .as_ref()
1318 .map(|d| d == fetched_digest)
1319 .unwrap_or_default();
1320 let booted_unchanged = booted_image
1321 .as_ref()
1322 .map(|img| &img.manifest_digest == fetched_digest)
1323 .unwrap_or_default();
1324 if staged_unchanged {
1325 let staged_deployment = storage.get_ostree()?.staged_deployment();
1326 let mut download_only_changed = false;
1327
1328 if let Some(staged) = staged_deployment {
1329 if opts.download_only {
1331 if !staged.is_finalization_locked() {
1333 storage.get_ostree()?.change_finalization(&staged)?;
1334 println!("Image downloaded, but will not be applied on reboot");
1335 download_only_changed = true;
1336 }
1337 } else if !opts.check {
1338 if staged.is_finalization_locked() {
1341 storage.get_ostree()?.change_finalization(&staged)?;
1342 println!("Staged deployment will now be applied on reboot");
1343 download_only_changed = true;
1344 }
1345 }
1346 } else if opts.download_only || opts.apply {
1347 anyhow::bail!("No staged deployment found");
1348 }
1349
1350 if !download_only_changed {
1351 println!("Staged update present, not changed");
1352 }
1353
1354 handle_staged_soft_reboot(booted_ostree, opts.soft_reboot, &host)?;
1355 if opts.apply {
1356 crate::reboot::reboot()?;
1357 }
1358 } else if booted_unchanged {
1359 println!("No update available.")
1360 } else {
1361 let stateroot = booted_ostree.stateroot();
1362 let from = MergeState::from_stateroot(storage, &stateroot)?;
1363 crate::deploy::stage(
1364 storage,
1365 from,
1366 &fetched,
1367 &spec,
1368 prog.clone(),
1369 opts.download_only,
1370 )
1371 .await?;
1372 changed = true;
1373 if let Some(prev) = booted_image.as_ref() {
1374 if let Some(fetched_manifest) = fetched.get_manifest(repo)? {
1375 let diff =
1376 ostree_container::ManifestDiff::new(&prev.manifest, &fetched_manifest);
1377 diff.print();
1378 }
1379 }
1380 }
1381 }
1382 if changed {
1383 storage.update_mtime()?;
1384
1385 if opts.soft_reboot.is_some() {
1386 let updated_host = crate::status::get_status(booted_ostree)?.1;
1389 handle_staged_soft_reboot(booted_ostree, opts.soft_reboot, &updated_host)?;
1390 }
1391
1392 if opts.apply {
1393 crate::reboot::reboot()?;
1394 }
1395 } else {
1396 tracing::debug!("No changes");
1397 }
1398
1399 Ok(())
1400}
1401pub(crate) fn imgref_for_switch(opts: &SwitchOpts) -> Result<ImageReference> {
1402 let transport = ostree_container::Transport::try_from(opts.transport.as_str())?;
1403 let imgref = ostree_container::ImageReference {
1404 transport,
1405 name: opts.target.to_string(),
1406 };
1407 let sigverify = sigpolicy_from_opt(opts.enforce_container_sigpolicy);
1408 let target = ostree_container::OstreeImageReference { sigverify, imgref };
1409 let target = ImageReference::from(target);
1410
1411 return Ok(target);
1412}
1413
1414#[context("Switching (ostree)")]
1416async fn switch_ostree(
1417 opts: SwitchOpts,
1418 storage: &Storage,
1419 booted_ostree: &BootedOstree<'_>,
1420) -> Result<()> {
1421 let target = imgref_for_switch(&opts)?;
1422 let prog: ProgressWriter = opts.progress.try_into()?;
1423 let cancellable = gio::Cancellable::NONE;
1424
1425 let repo = &booted_ostree.repo();
1426 let (_, host) = crate::status::get_status(booted_ostree)?;
1427
1428 let new_spec = {
1429 let mut new_spec = host.spec.clone();
1430 new_spec.image = Some(target.clone());
1431 new_spec
1432 };
1433
1434 if new_spec == host.spec {
1435 println!("Image specification is unchanged.");
1436 if opts.apply && host.status.staged.is_some() {
1437 crate::reboot::reboot()?;
1438 }
1439 return Ok(());
1440 }
1441
1442 const SWITCH_JOURNAL_ID: &str = "7a6b5c4d3e2f1a0b9c8d7e6f5a4b3c2d1";
1444 let old_image = host
1445 .spec
1446 .image
1447 .as_ref()
1448 .map(|i| i.image.as_str())
1449 .unwrap_or("none");
1450
1451 tracing::info!(
1452 message_id = SWITCH_JOURNAL_ID,
1453 bootc.old_image_reference = old_image,
1454 bootc.new_image_reference = &target.image,
1455 bootc.new_image_transport = &target.transport,
1456 "Switching from image {} to {}",
1457 old_image,
1458 target.image
1459 );
1460
1461 let new_spec = RequiredHostSpec::from_spec(&new_spec)?;
1462
1463 let use_unified = if opts.unified_storage_exp {
1467 true
1468 } else {
1469 crate::deploy::image_exists_in_unified_storage(storage, &target).await?
1470 };
1471
1472 let fetched = if use_unified {
1473 crate::deploy::pull_unified(
1474 repo,
1475 &target,
1476 None,
1477 opts.quiet,
1478 prog.clone(),
1479 storage,
1480 Some(&booted_ostree.deployment),
1481 )
1482 .await?
1483 } else {
1484 crate::deploy::pull(
1485 repo,
1486 &target,
1487 None,
1488 opts.quiet,
1489 prog.clone(),
1490 Some(&booted_ostree.deployment),
1491 )
1492 .await?
1493 };
1494
1495 if !opts.retain {
1496 if let Some(booted_origin) = booted_ostree.deployment.origin() {
1498 if let Some(ostree_ref) = booted_origin.optional_string("origin", "refspec")? {
1499 let (remote, ostree_ref) =
1500 ostree::parse_refspec(&ostree_ref).context("Failed to parse ostree ref")?;
1501 repo.set_ref_immediate(remote.as_deref(), &ostree_ref, None, cancellable)?;
1502 }
1503 }
1504 }
1505
1506 let stateroot = booted_ostree.stateroot();
1507 let from = MergeState::from_stateroot(storage, &stateroot)?;
1508 crate::deploy::stage(storage, from, &fetched, &new_spec, prog.clone(), false).await?;
1509
1510 storage.update_mtime()?;
1511
1512 if opts.soft_reboot.is_some() {
1513 let updated_host = crate::status::get_status(booted_ostree)?.1;
1516 handle_staged_soft_reboot(booted_ostree, opts.soft_reboot, &updated_host)?;
1517 }
1518
1519 if opts.apply {
1520 crate::reboot::reboot()?;
1521 }
1522
1523 Ok(())
1524}
1525
1526#[context("Switching")]
1528async fn switch(opts: SwitchOpts) -> Result<()> {
1529 if opts.mutate_in_place {
1533 let target = imgref_for_switch(&opts)?;
1534 let deployid = {
1535 let target = target.clone();
1537 let root = cap_std::fs::Dir::open_ambient_dir("/", cap_std::ambient_authority())?;
1538 tokio::task::spawn_blocking(move || {
1539 crate::deploy::switch_origin_inplace(&root, &target)
1540 })
1541 .await??
1542 };
1543 println!("Updated {deployid} to pull from {target}");
1544 return Ok(());
1545 }
1546 let storage = &get_storage().await?;
1547 match storage.kind()? {
1548 BootedStorageKind::Ostree(booted_ostree) => {
1549 switch_ostree(opts, storage, &booted_ostree).await
1550 }
1551 BootedStorageKind::Composefs(booted_cfs) => {
1552 switch_composefs(opts, storage, &booted_cfs).await
1553 }
1554 }
1555}
1556
1557#[context("Rollback (ostree)")]
1559async fn rollback_ostree(
1560 opts: &RollbackOpts,
1561 storage: &Storage,
1562 booted_ostree: &BootedOstree<'_>,
1563) -> Result<()> {
1564 crate::deploy::rollback(storage).await?;
1565
1566 if opts.soft_reboot.is_some() {
1567 let host = crate::status::get_status(booted_ostree)?.1;
1569
1570 handle_soft_reboot(
1571 opts.soft_reboot,
1572 host.status.rollback.as_ref(),
1573 "rollback",
1574 || soft_reboot_rollback(booted_ostree),
1575 )?;
1576 }
1577
1578 Ok(())
1579}
1580
1581#[context("Rollback")]
1583async fn rollback(opts: &RollbackOpts) -> Result<()> {
1584 let storage = &get_storage().await?;
1585 match storage.kind()? {
1586 BootedStorageKind::Ostree(booted_ostree) => {
1587 rollback_ostree(opts, storage, &booted_ostree).await
1588 }
1589 BootedStorageKind::Composefs(booted_cfs) => composefs_rollback(storage, &booted_cfs).await,
1590 }
1591}
1592
1593#[context("Editing spec (ostree)")]
1595async fn edit_ostree(
1596 opts: EditOpts,
1597 storage: &Storage,
1598 booted_ostree: &BootedOstree<'_>,
1599) -> Result<()> {
1600 let repo = &booted_ostree.repo();
1601 let (_, host) = crate::status::get_status(booted_ostree)?;
1602
1603 let new_host: Host = if let Some(filename) = opts.filename {
1604 let mut r = std::io::BufReader::new(std::fs::File::open(filename)?);
1605 serde_yaml::from_reader(&mut r)?
1606 } else {
1607 let tmpf = tempfile::NamedTempFile::with_suffix(".yaml")?;
1608 serde_yaml::to_writer(std::io::BufWriter::new(tmpf.as_file()), &host)?;
1609 crate::utils::spawn_editor(&tmpf)?;
1610 tmpf.as_file().seek(std::io::SeekFrom::Start(0))?;
1611 serde_yaml::from_reader(&mut tmpf.as_file())?
1612 };
1613
1614 if new_host.spec == host.spec {
1615 println!("Edit cancelled, no changes made.");
1616 return Ok(());
1617 }
1618 host.spec.verify_transition(&new_host.spec)?;
1619 let new_spec = RequiredHostSpec::from_spec(&new_host.spec)?;
1620
1621 let prog = ProgressWriter::default();
1622
1623 if host.spec.boot_order != new_host.spec.boot_order {
1626 return crate::deploy::rollback(storage).await;
1627 }
1628
1629 let fetched = crate::deploy::pull(
1630 repo,
1631 new_spec.image,
1632 None,
1633 opts.quiet,
1634 prog.clone(),
1635 Some(&booted_ostree.deployment),
1636 )
1637 .await?;
1638
1639 let stateroot = booted_ostree.stateroot();
1642 let from = MergeState::from_stateroot(storage, &stateroot)?;
1643 crate::deploy::stage(storage, from, &fetched, &new_spec, prog.clone(), false).await?;
1644
1645 storage.update_mtime()?;
1646
1647 Ok(())
1648}
1649
1650#[context("Editing spec")]
1652async fn edit(opts: EditOpts) -> Result<()> {
1653 let storage = &get_storage().await?;
1654 match storage.kind()? {
1655 BootedStorageKind::Ostree(booted_ostree) => {
1656 edit_ostree(opts, storage, &booted_ostree).await
1657 }
1658 BootedStorageKind::Composefs(_) => {
1659 anyhow::bail!("Edit is not yet supported for composefs backend")
1660 }
1661 }
1662}
1663
1664async fn usroverlay(access_mode: FilesystemOverlayAccessMode) -> Result<()> {
1666 let args = match access_mode {
1669 FilesystemOverlayAccessMode::ReadOnly => ["admin", "unlock", "--transient"].as_slice(),
1671
1672 FilesystemOverlayAccessMode::ReadWrite => ["admin", "unlock"].as_slice(),
1673 };
1674 Err(Command::new("ostree").args(args).exec().into())
1675}
1676
1677fn join_host_ipc_namespace() -> Result<()> {
1690 let caps = rustix::thread::capabilities(None).context("capget")?;
1691 if !caps
1692 .effective
1693 .contains(rustix::thread::CapabilitySet::SYS_ADMIN)
1694 {
1695 return Ok(());
1696 }
1697 let ns_pid1 = match std::fs::read_link("/proc/1/ns/ipc") {
1698 Ok(v) => v,
1699 Err(e) if e.kind() == std::io::ErrorKind::PermissionDenied => {
1700 return Ok(());
1701 }
1702 Err(e) => return Err(e).context("reading /proc/1/ns/ipc"),
1703 };
1704 let ns_self = std::fs::read_link("/proc/self/ns/ipc").context("reading /proc/self/ns/ipc")?;
1705 if ns_pid1 != ns_self {
1706 let pid1ipcns = std::fs::File::open("/proc/1/ns/ipc").context("open pid1 ipcns")?;
1707 rustix::thread::move_into_link_name_space(
1708 pid1ipcns.as_fd(),
1709 Some(rustix::thread::LinkNameSpaceType::InterProcessCommunication),
1710 )
1711 .context("setns(ipc)")?;
1712 }
1713 Ok(())
1714}
1715
1716#[allow(unsafe_code)]
1719pub fn global_init() -> Result<()> {
1720 join_host_ipc_namespace()?;
1721 ostree::glib::set_prgname(bootc_utils::NAME.into());
1724 if let Err(e) = rustix::thread::set_name(&CString::new(bootc_utils::NAME).unwrap()) {
1725 eprintln!("failed to set name: {e}");
1727 }
1728 ostree::SePolicy::set_null_log();
1730 let am_root = rustix::process::getuid().is_root();
1731 if std::env::var_os("HOME").is_none() && am_root {
1734 unsafe {
1739 std::env::set_var("HOME", "/root");
1740 }
1741 }
1742 Ok(())
1743}
1744
1745pub async fn run_from_iter<I>(args: I) -> Result<()>
1748where
1749 I: IntoIterator,
1750 I::Item: Into<OsString> + Clone,
1751{
1752 run_from_opt(Opt::parse_including_static(args)).await
1753}
1754
1755fn callname_from_argv0(argv0: &OsStr) -> &str {
1759 let default = "bootc";
1760 std::path::Path::new(argv0)
1761 .file_name()
1762 .and_then(|s| s.to_str())
1763 .filter(|s| !s.is_empty())
1764 .unwrap_or(default)
1765}
1766
1767impl Opt {
1768 fn parse_including_static<I>(args: I) -> Self
1771 where
1772 I: IntoIterator,
1773 I::Item: Into<OsString> + Clone,
1774 {
1775 let mut args = args.into_iter();
1776 let first = if let Some(first) = args.next() {
1777 let first: OsString = first.into();
1778 let argv0 = callname_from_argv0(&first);
1779 tracing::debug!("argv0={argv0:?}");
1780 let mapped = match argv0 {
1781 InternalsOpts::GENERATOR_BIN => {
1782 Some(["bootc", "internals", "systemd-generator"].as_slice())
1783 }
1784 "ostree-container" | "ostree-ima-sign" | "ostree-provisional-repair" => {
1785 Some(["bootc", "internals", "ostree-ext"].as_slice())
1786 }
1787 _ => None,
1788 };
1789 if let Some(base_args) = mapped {
1790 let base_args = base_args.iter().map(OsString::from);
1791 return Opt::parse_from(base_args.chain(args.map(|i| i.into())));
1792 }
1793 Some(first)
1794 } else {
1795 None
1796 };
1797 Opt::parse_from(first.into_iter().chain(args.map(|i| i.into())))
1798 }
1799}
1800
1801async fn run_from_opt(opt: Opt) -> Result<()> {
1803 let root = &Dir::open_ambient_dir("/", cap_std::ambient_authority())?;
1804 match opt {
1805 Opt::Upgrade(opts) => {
1806 let storage = &get_storage().await?;
1807 match storage.kind()? {
1808 BootedStorageKind::Ostree(booted_ostree) => {
1809 upgrade(opts, storage, &booted_ostree).await
1810 }
1811 BootedStorageKind::Composefs(booted_cfs) => {
1812 upgrade_composefs(opts, storage, &booted_cfs).await
1813 }
1814 }
1815 }
1816 Opt::Switch(opts) => switch(opts).await,
1817 Opt::Rollback(opts) => {
1818 rollback(&opts).await?;
1819 if opts.apply {
1820 crate::reboot::reboot()?;
1821 }
1822 Ok(())
1823 }
1824 Opt::Edit(opts) => edit(opts).await,
1825 Opt::UsrOverlay(opts) => {
1826 use crate::store::Environment;
1827 let env = Environment::detect()?;
1828 let access_mode = if opts.read_only {
1829 FilesystemOverlayAccessMode::ReadOnly
1830 } else {
1831 FilesystemOverlayAccessMode::ReadWrite
1832 };
1833 match env {
1834 Environment::OstreeBooted => usroverlay(access_mode).await,
1835 Environment::ComposefsBooted(_) => composefs_usr_overlay(access_mode),
1836 _ => anyhow::bail!("usroverlay only applies on booted hosts"),
1837 }
1838 }
1839 Opt::Container(opts) => match opts {
1840 ContainerOpts::Inspect {
1841 rootfs,
1842 json,
1843 format,
1844 } => crate::status::container_inspect(&rootfs, json, format),
1845 ContainerOpts::Lint {
1846 rootfs,
1847 fatal_warnings,
1848 list,
1849 skip,
1850 no_truncate,
1851 } => {
1852 if list {
1853 return lints::lint_list(std::io::stdout().lock());
1854 }
1855 let warnings = if fatal_warnings {
1856 lints::WarningDisposition::FatalWarnings
1857 } else {
1858 lints::WarningDisposition::AllowWarnings
1859 };
1860 let root_type = if rootfs == "/" {
1861 lints::RootType::Running
1862 } else {
1863 lints::RootType::Alternative
1864 };
1865
1866 let root = &Dir::open_ambient_dir(rootfs, cap_std::ambient_authority())?;
1867 let skip = skip.iter().map(|s| s.as_str());
1868 lints::lint(
1869 root,
1870 warnings,
1871 root_type,
1872 skip,
1873 std::io::stdout().lock(),
1874 no_truncate,
1875 )?;
1876 Ok(())
1877 }
1878 ContainerOpts::SplitKernelAndRootfs { rootfs, output } => {
1879 use crate::kernel::{KernelType, find_kernel};
1880
1881 let root = Dir::open_ambient_dir(&rootfs, ambient_authority())?;
1882
1883 let kernel_internal = find_kernel(&root)?
1884 .ok_or_else(|| anyhow::anyhow!("No kernel found in rootfs"))?;
1885
1886 if kernel_internal.kernel.unified {
1887 anyhow::bail!("UKIs are not supported");
1888 }
1889
1890 match &kernel_internal.k_type {
1891 KernelType::Vmlinuz { path, initramfs } => {
1892 let kver = &kernel_internal.kernel.version;
1893 let kernel_output_dir = output.join(kver);
1894 std::fs::create_dir_all(&kernel_output_dir)?;
1895
1896 let vmlinuz_src = rootfs.join(path);
1897 let initramfs_src = rootfs.join(initramfs);
1898 let vmlinuz_dst = kernel_output_dir.join("vmlinuz");
1899 let initramfs_dst = kernel_output_dir.join("initramfs.img");
1900
1901 std::fs::rename(&vmlinuz_src, &vmlinuz_dst).context("Moving vmlinuz")?;
1902 std::fs::rename(&initramfs_src, &initramfs_dst)
1903 .context("Moving initramfs")?;
1904 }
1905
1906 KernelType::Uki { .. } => {
1907 anyhow::bail!("UKIs are not supported");
1908 }
1909 }
1910
1911 Ok(())
1912 }
1913 ContainerOpts::ComputeComposefsDigest {
1914 path,
1915 write_dumpfile_to,
1916 } => {
1917 let digest = compute_composefs_digest(&path, write_dumpfile_to.as_deref()).await?;
1918 println!("{digest}");
1919 Ok(())
1920 }
1921 ContainerOpts::ComputeComposefsDigestFromStorage {
1922 write_dumpfile_to,
1923 image,
1924 } => {
1925 let (_td_guard, repo) = new_temp_composefs_repo()?;
1926
1927 let mut proxycfg = crate::deploy::new_proxy_config();
1928
1929 let image = if let Some(image) = image {
1930 image
1931 } else {
1932 let host_container_store = Utf8Path::new("/run/host-container-storage");
1933 let container_info = crate::containerenv::get_container_execution_info(&root)?;
1936 let iid = container_info.imageid;
1937 tracing::debug!("Computing digest of {iid}");
1938
1939 if !host_container_store.try_exists()? {
1940 anyhow::bail!(
1941 "Must be readonly mount of host container store: {host_container_store}"
1942 );
1943 }
1944 let mut cmd = Command::new(bootc_utils::skopeo_bin());
1946 set_additional_image_store(&mut cmd, "/run/host-container-storage");
1947 proxycfg.skopeo_cmd = Some(cmd);
1948 iid
1949 };
1950
1951 let imgref = format!("containers-storage:{image}");
1952 let host_store = std::path::Path::new("/run/host-container-storage");
1953 let opts = composefs_oci::PullOptions {
1954 img_proxy_config: Some(proxycfg),
1955 additional_image_stores: &[host_store],
1956 ..Default::default()
1957 };
1958 let pull_result = composefs_oci::pull(&repo, &imgref, None, opts)
1959 .await
1960 .context("Pulling image")?;
1961 let mut fs = composefs_oci::image::create_filesystem(
1962 &repo,
1963 &pull_result.config_digest,
1964 Some(&pull_result.config_verity),
1965 )
1966 .context("Populating fs")?;
1967 fs.transform_for_boot(&repo).context("Preparing for boot")?;
1968 let id = fs.compute_image_id(repo.erofs_version());
1969 println!("{}", id.to_hex());
1970
1971 if let Some(path) = write_dumpfile_to.as_deref() {
1972 let mut w = File::create(path)
1973 .with_context(|| format!("Opening {path}"))
1974 .map(BufWriter::new)?;
1975 dumpfile::write_dumpfile(&mut w, &fs).context("Writing dumpfile")?;
1976 }
1977
1978 Ok(())
1979 }
1980 ContainerOpts::Ukify {
1981 rootfs,
1982 kargs,
1983 allow_missing_verity,
1984 write_dumpfile_to,
1985 kernel_dir,
1986 args,
1987 } => {
1988 let kernel = match kernel_dir {
1989 Some(kernel_dir) => {
1990 let kver = kernel_dir
1991 .components()
1992 .last()
1993 .ok_or_else(|| anyhow::anyhow!("Could not determine kernel version"))?;
1994
1995 Some(crate::kernel::KernelInternal {
1996 kernel: crate::kernel::Kernel {
1997 unified: false,
1998 version: kver.to_string(),
1999 },
2000 k_type: crate::kernel::KernelType::Vmlinuz {
2001 path: kernel_dir.join("vmlinuz"),
2002 initramfs: kernel_dir.join("initramfs.img"),
2003 },
2004 })
2005 }
2006
2007 None => None,
2008 };
2009
2010 crate::ukify::build_ukify(
2011 &rootfs,
2012 &kargs,
2013 &args,
2014 kernel,
2015 allow_missing_verity,
2016 write_dumpfile_to.as_deref(),
2017 )
2018 .await
2019 }
2020 ContainerOpts::Export {
2021 format,
2022 target,
2023 output,
2024 kernel_in_boot,
2025 disable_selinux,
2026 } => {
2027 crate::container_export::export(
2028 &format,
2029 &target,
2030 output.as_deref(),
2031 kernel_in_boot,
2032 disable_selinux,
2033 )
2034 .await
2035 }
2036 },
2037 Opt::Completion { shell } => {
2038 use clap_complete::aot::generate;
2039
2040 let mut cmd = Opt::command();
2041 let mut stdout = std::io::stdout();
2042 let bin_name = "bootc";
2043 generate(shell, &mut cmd, bin_name, &mut stdout);
2044 Ok(())
2045 }
2046 Opt::Image(opts) => match opts {
2047 ImageOpts::List {
2048 list_type,
2049 list_format,
2050 } => crate::image::list_entrypoint(list_type, list_format).await,
2051
2052 ImageOpts::CopyToStorage { source, target } => {
2053 let host = get_host().await?;
2055
2056 let storage = get_storage().await?;
2057
2058 match storage.kind()? {
2059 BootedStorageKind::Ostree(..) => {
2060 crate::image::push_entrypoint(
2061 &storage,
2062 &host,
2063 source.as_deref(),
2064 target.as_deref(),
2065 )
2066 .await
2067 }
2068 BootedStorageKind::Composefs(booted) => {
2069 bootc_composefs::export::export_repo_to_image(
2070 &storage,
2071 &booted,
2072 source.as_deref(),
2073 target.as_deref(),
2074 )
2075 .await
2076 }
2077 }
2078 }
2079 ImageOpts::SetUnified => crate::image::set_unified_entrypoint().await,
2080 ImageOpts::PullFromDefaultStorage { image } => {
2081 let storage = get_storage().await?;
2082 storage
2083 .get_ensure_imgstore()?
2084 .pull_from_host_storage(&image)
2085 .await
2086 }
2087 ImageOpts::Cmd(opt) => {
2088 let storage = get_storage().await?;
2089 let imgstore = storage.get_ensure_imgstore()?;
2090 match opt {
2091 ImageCmdOpts::List { args } => {
2092 crate::image::imgcmd_entrypoint(imgstore, "list", &args).await
2093 }
2094 ImageCmdOpts::Build { args } => {
2095 crate::image::imgcmd_entrypoint(imgstore, "build", &args).await
2096 }
2097 ImageCmdOpts::Pull { images } => {
2098 for image in &images {
2099 imgstore.pull_with_progress(image).await?;
2100 }
2101 Ok(())
2102 }
2103 ImageCmdOpts::Push { args } => {
2104 crate::image::imgcmd_entrypoint(imgstore, "push", &args).await
2105 }
2106 }
2107 }
2108 },
2109 Opt::Install(opts) => match opts {
2110 #[cfg(feature = "install-to-disk")]
2111 InstallOpts::ToDisk(opts) => crate::install::install_to_disk(opts).await,
2112 InstallOpts::ToFilesystem(opts) => {
2113 crate::install::install_to_filesystem(opts, false, crate::install::Cleanup::Skip)
2114 .await
2115 }
2116 InstallOpts::ToExistingRoot(opts) => {
2117 crate::install::install_to_existing_root(opts).await
2118 }
2119 InstallOpts::Reset(opts) => crate::install::install_reset(opts).await,
2120 InstallOpts::PrintConfiguration(opts) => crate::install::print_configuration(opts),
2121 InstallOpts::EnsureCompletion {} => {
2122 let rootfs = &Dir::open_ambient_dir("/", cap_std::ambient_authority())?;
2123 crate::install::completion::run_from_anaconda(rootfs).await
2124 }
2125 InstallOpts::Finalize { root_path } => {
2126 crate::install::install_finalize(&root_path).await
2127 }
2128 },
2129 Opt::LoaderEntries(opts) => match opts {
2130 LoaderEntriesOpts::SetOptionsForSource(opts) => {
2131 let storage = get_storage().await?;
2132 let sysroot = storage.get_ostree()?;
2133 crate::loader_entries::set_options_for_source_staged(
2134 sysroot,
2135 &opts.source,
2136 opts.options.as_deref(),
2137 )?;
2138 Ok(())
2139 }
2140 },
2141 Opt::ExecInHostMountNamespace { args } => {
2142 crate::install::exec_in_host_mountns(args.as_slice())
2143 }
2144 Opt::Status(opts) => super::status::status(opts).await,
2145 Opt::Internals(opts) => match opts {
2146 InternalsOpts::SystemdGenerator {
2147 normal_dir,
2148 early_dir: _,
2149 late_dir: _,
2150 } => {
2151 let unit_dir = &Dir::open_ambient_dir(normal_dir, cap_std::ambient_authority())?;
2152 crate::generator::generator(root, unit_dir)
2153 }
2154 InternalsOpts::OstreeExt { args } => {
2155 ostree_ext::cli::run_from_iter(["ostree-ext".into()].into_iter().chain(args)).await
2156 }
2157 InternalsOpts::OstreeContainer { args } => {
2158 ostree_ext::cli::run_from_iter(
2159 ["ostree-ext".into(), "container".into()]
2160 .into_iter()
2161 .chain(args),
2162 )
2163 .await
2164 }
2165 InternalsOpts::TestComposefs => {
2166 let storage = get_storage().await?;
2168 let cfs = storage.get_ensure_composefs()?;
2169 let testdata = b"some test data";
2170 let testdata_digest = hex::encode(openssl::sha::sha256(testdata));
2171 let mut w = cfs.create_stream(0)?;
2172 w.write_inline(testdata);
2173 let object = cfs
2174 .write_stream(w, &testdata_digest, Some("testobject"))?
2175 .to_hex();
2176 assert_eq!(
2177 object,
2178 "84245c6936db9939dda9c1fbeafdcbd2b49f7605354c88d4f016c4d941551f45bad0fbcdbee12ba8adfe4fb63541de57ac02729edbacdb556325e342b89d340d"
2179 );
2180 Ok(())
2181 }
2182 InternalsOpts::Fsverity(args) => match args {
2184 FsverityOpts::Measure { path } => {
2185 let fd =
2186 std::fs::File::open(&path).with_context(|| format!("Reading {path}"))?;
2187 let digest: fsverity::Sha256HashValue = fsverity::measure_verity(&fd)?;
2188 let digest = digest.to_hex();
2189 println!("{digest}");
2190 Ok(())
2191 }
2192 FsverityOpts::Enable { path } => {
2193 let fd =
2194 std::fs::File::open(&path).with_context(|| format!("Reading {path}"))?;
2195 fsverity::enable_verity_raw::<fsverity::Sha256HashValue>(&fd)?;
2196 Ok(())
2197 }
2198 },
2199 InternalsOpts::Cfs { args } => composefs_ctl::run_from_iter(args.iter()).await,
2200 InternalsOpts::Reboot => crate::reboot::reboot(),
2201 InternalsOpts::Fsck => {
2202 let storage = &get_storage().await?;
2203 crate::fsck::fsck(&storage, std::io::stdout().lock()).await?;
2204 Ok(())
2205 }
2206 InternalsOpts::FixupEtcFstab => crate::deploy::fixup_etc_fstab(&root),
2207 InternalsOpts::SysusersSync => crate::sysusers_cleanup::run(&root),
2208 InternalsOpts::PrintJsonSchema { of } => {
2209 let schema = match of {
2210 SchemaType::Host => schema_for!(crate::spec::Host),
2211 SchemaType::Progress => schema_for!(crate::progress_jsonl::Event),
2212 };
2213 let mut stdout = std::io::stdout().lock();
2214 serde_json::to_writer_pretty(&mut stdout, &schema)?;
2215 Ok(())
2216 }
2217 InternalsOpts::Cleanup => {
2218 let storage = get_storage().await?;
2219 crate::deploy::cleanup(&storage).await
2220 }
2221 InternalsOpts::Relabel { as_path, path } => {
2222 let root = &Dir::open_ambient_dir("/", cap_std::ambient_authority())?;
2223 let path = path.strip_prefix("/")?;
2224 let sepolicy =
2225 &ostree::SePolicy::new(&gio::File::for_path("/"), gio::Cancellable::NONE)?;
2226 crate::lsm::relabel_recurse(root, path, as_path.as_deref(), sepolicy)?;
2227 Ok(())
2228 }
2229 InternalsOpts::RelabelOverlayMountpoints => {
2230 crate::generator::relabel_overlay_mountpoints()
2231 }
2232 InternalsOpts::BootcInstallCompletion { sysroot, stateroot } => {
2233 let rootfs = &Dir::open_ambient_dir("/", cap_std::ambient_authority())?;
2234 crate::install::completion::run_from_ostree(rootfs, &sysroot, &stateroot).await
2235 }
2236 InternalsOpts::LoopbackCleanupHelper { device } => {
2237 crate::blockdev::run_loopback_cleanup_helper(&device).await
2238 }
2239 InternalsOpts::AllocateCleanupLoopback { file_path: _ } => {
2240 let temp_file =
2242 tempfile::NamedTempFile::new().context("Failed to create temporary file")?;
2243 let temp_path = temp_file.path();
2244
2245 let loopback = crate::blockdev::LoopbackDevice::new(temp_path)
2247 .context("Failed to create loopback device")?;
2248
2249 println!("Created loopback device: {}", loopback.path());
2250
2251 loopback
2253 .close()
2254 .context("Failed to close loopback device")?;
2255
2256 println!("Successfully closed loopback device");
2257 Ok(())
2258 }
2259 #[cfg(feature = "rhsm")]
2260 InternalsOpts::PublishRhsmFacts => crate::rhsm::publish_facts(&root).await,
2261 #[cfg(feature = "docgen")]
2262 InternalsOpts::DumpCliJson => {
2263 use clap::CommandFactory;
2264 let cmd = Opt::command();
2265 let json = crate::cli_json::dump_cli_json(&cmd)?;
2266 println!("{}", json);
2267 Ok(())
2268 }
2269 InternalsOpts::DirDiff {
2270 pristine_etc,
2271 current_etc,
2272 new_etc,
2273 merge,
2274 } => {
2275 let pristine_etc =
2276 Dir::open_ambient_dir(pristine_etc, cap_std::ambient_authority())?;
2277 let current_etc = Dir::open_ambient_dir(current_etc, cap_std::ambient_authority())?;
2278 let new_etc = Dir::open_ambient_dir(new_etc, cap_std::ambient_authority())?;
2279
2280 let (p, c, n) =
2281 etc_merge::traverse_etc(&pristine_etc, ¤t_etc, Some(&new_etc))?;
2282
2283 let n = n
2284 .as_ref()
2285 .ok_or_else(|| anyhow::anyhow!("Failed to get new directory tree"))?;
2286
2287 let diff = compute_diff(&p, &c, &n)?;
2288 print_diff(&diff, &mut std::io::stdout());
2289
2290 if merge {
2291 etc_merge::merge(¤t_etc, &c, &new_etc, &n, &diff)?;
2292 }
2293
2294 Ok(())
2295 }
2296 InternalsOpts::PrepSoftReboot {
2297 deployment,
2298 reboot,
2299 reset,
2300 } => {
2301 let storage = &get_storage().await?;
2302
2303 match storage.kind()? {
2304 BootedStorageKind::Ostree(..) => {
2305 anyhow::bail!("soft-reboot only implemented for composefs")
2307 }
2308
2309 BootedStorageKind::Composefs(booted_cfs) => {
2310 if reset {
2311 return reset_soft_reboot();
2312 }
2313
2314 prepare_soft_reboot_composefs(
2315 &storage,
2316 &booted_cfs,
2317 deployment.as_deref(),
2318 SoftRebootMode::Required,
2319 reboot,
2320 )
2321 .await
2322 }
2323 }
2324 }
2325 InternalsOpts::ComposefsGC {
2326 dry_run,
2327 assert_no_op,
2328 prune_repo,
2329 } => {
2330 let storage = &get_storage().await?;
2331
2332 match storage.kind()? {
2333 BootedStorageKind::Ostree(..) => {
2334 anyhow::bail!("composefs-gc only works for composefs backend");
2335 }
2336
2337 BootedStorageKind::Composefs(booted_cfs) => {
2338 let dry_run = dry_run || assert_no_op;
2339 let gc_result = composefs_gc(
2340 storage,
2341 &booted_cfs,
2342 GCOpts {
2343 dry_run,
2344 prune_repo,
2345 },
2346 )
2347 .await?;
2348
2349 if dry_run {
2350 println!("Dry run (no files deleted)");
2351 }
2352
2353 println!(
2354 "Objects: {} removed ({} bytes)",
2355 gc_result.objects_removed, gc_result.objects_bytes
2356 );
2357
2358 if gc_result.images_pruned > 0 || gc_result.streams_pruned > 0 {
2359 println!(
2360 "Pruned symlinks: {} images, {} streams",
2361 gc_result.images_pruned, gc_result.streams_pruned
2362 );
2363 }
2364
2365 if assert_no_op {
2366 let is_noop = gc_result.objects_removed == 0
2367 && gc_result.images_pruned == 0
2368 && gc_result.streams_pruned == 0;
2369 if !is_noop {
2370 anyhow::bail!(
2371 "--assert-no-op: GC would remove {} object(s), {} image symlink(s), {} stream symlink(s) (issue #1808)",
2372 gc_result.objects_removed,
2373 gc_result.images_pruned,
2374 gc_result.streams_pruned,
2375 );
2376 }
2377 }
2378
2379 Ok(())
2380 }
2381 }
2382 }
2383 InternalsOpts::Blockdev(opts) => {
2384 let dev = match opts {
2385 BlockdevOpts::Ls { device } => crate::blockdev::list_dev(&device)?,
2386 BlockdevOpts::LsFilesystem { path } => {
2387 let dir = Dir::open_ambient_dir(&path, cap_std::ambient_authority())?;
2388 crate::blockdev::list_dev_by_dir(&dir)?
2389 }
2390 };
2391 serde_json::to_writer_pretty(std::io::stdout().lock(), &dev)?;
2392 println!();
2393 Ok(())
2394 }
2395 InternalsOpts::Uki(uki_opts) => match uki_opts {
2396 UkiSubcommands::Extract { path, output_path } => {
2397 let mut uki_file =
2398 std::fs::File::open(&path).with_context(|| format!("Opening {path}"))?;
2399
2400 let uname =
2401 composefs_boot::uki::get_text_section_buffered(&mut uki_file, ".uname")
2402 .context("Getting uname")?;
2403
2404 std::fs::create_dir_all(&output_path).context("Creating output directory")?;
2405
2406 let output_dir = Dir::open_ambient_dir(&output_path, ambient_authority())
2407 .context("Opening output dir")?;
2408 output_dir.create_dir(&uname)?;
2409
2410 let output_dir = output_dir.open_dir(&uname)?;
2411
2412 for (section_name, file_name) in
2413 [(".linux", "vmlinuz"), (".initrd", "initramfs.img")]
2414 {
2415 uki_file
2416 .seek(SeekFrom::Start(0))
2417 .context("Seeking to start")?;
2418 let section =
2419 composefs_boot::uki::get_section_buffered(&mut uki_file, section_name)
2420 .with_context(|| format!("Getting {section_name} section"))?;
2421 output_dir
2422 .write(file_name, section)
2423 .with_context(|| format!("Writing {file_name}"))?;
2424 }
2425
2426 Ok(())
2427 }
2428 },
2429 },
2430 Opt::State(opts) => match opts {
2431 StateOpts::WipeOstree => {
2432 let sysroot = ostree::Sysroot::new_default();
2433 sysroot.load(gio::Cancellable::NONE)?;
2434 crate::deploy::wipe_ostree(sysroot).await?;
2435 Ok(())
2436 }
2437 },
2438
2439 Opt::ComposefsFinalizeStaged => {
2440 let storage = &get_storage().await?;
2441 match storage.kind()? {
2442 BootedStorageKind::Ostree(_) => {
2443 anyhow::bail!("ComposefsFinalizeStaged is only supported for composefs backend")
2444 }
2445 BootedStorageKind::Composefs(booted_cfs) => {
2446 composefs_backend_finalize(storage, &booted_cfs).await
2447 }
2448 }
2449 }
2450
2451 Opt::ConfigDiff => {
2452 let storage = &get_storage().await?;
2453 match storage.kind()? {
2454 BootedStorageKind::Ostree(_) => {
2455 anyhow::bail!("ConfigDiff is only supported for composefs backend")
2456 }
2457 BootedStorageKind::Composefs(booted_cfs) => {
2458 get_etc_diff(storage, &booted_cfs).await
2459 }
2460 }
2461 }
2462
2463 Opt::DeleteDeployment { depl_id } => {
2464 let storage = &get_storage().await?;
2465 match storage.kind()? {
2466 BootedStorageKind::Ostree(_) => {
2467 anyhow::bail!("DeleteDeployment is only supported for composefs backend")
2468 }
2469 BootedStorageKind::Composefs(booted_cfs) => {
2470 delete_composefs_deployment(&depl_id, storage, &booted_cfs).await
2471 }
2472 }
2473 }
2474 }
2475}
2476
2477#[cfg(test)]
2478mod tests {
2479 use super::*;
2480
2481 #[test]
2482 fn test_callname() {
2483 use std::os::unix::ffi::OsStrExt;
2484
2485 let mapped_cases = [
2487 ("", "bootc"),
2488 ("/foo/bar", "bar"),
2489 ("/foo/bar/", "bar"),
2490 ("foo/bar", "bar"),
2491 ("../foo/bar", "bar"),
2492 ("usr/bin/ostree-container", "ostree-container"),
2493 ];
2494 for (input, output) in mapped_cases {
2495 assert_eq!(
2496 output,
2497 callname_from_argv0(OsStr::new(input)),
2498 "Handling mapped case {input}"
2499 );
2500 }
2501
2502 assert_eq!("bootc", callname_from_argv0(OsStr::from_bytes(b"foo\x80")));
2504
2505 let ident_cases = ["foo", "bootc"];
2507 for case in ident_cases {
2508 assert_eq!(
2509 case,
2510 callname_from_argv0(OsStr::new(case)),
2511 "Handling ident case {case}"
2512 );
2513 }
2514 }
2515
2516 #[test]
2517 fn test_parse_install_args() {
2518 let o = Opt::try_parse_from([
2520 "bootc",
2521 "install",
2522 "to-filesystem",
2523 "--target-no-signature-verification",
2524 "/target",
2525 ])
2526 .unwrap();
2527 let o = match o {
2528 Opt::Install(InstallOpts::ToFilesystem(fsopts)) => fsopts,
2529 o => panic!("Expected filesystem opts, not {o:?}"),
2530 };
2531 assert!(o.target_opts.target_no_signature_verification);
2532 assert_eq!(o.filesystem_opts.root_path.as_str(), "/target");
2533 assert_eq!(
2535 o.config_opts.bound_images,
2536 crate::install::BoundImagesOpt::Stored
2537 );
2538 }
2539
2540 #[test]
2541 fn test_parse_opts() {
2542 assert!(matches!(
2543 Opt::parse_including_static(["bootc", "status"]),
2544 Opt::Status(StatusOpts {
2545 json: false,
2546 format: None,
2547 format_version: None,
2548 booted: false,
2549 verbose: false
2550 })
2551 ));
2552 assert!(matches!(
2553 Opt::parse_including_static(["bootc", "status", "--format-version=0"]),
2554 Opt::Status(StatusOpts {
2555 format_version: Some(0),
2556 ..
2557 })
2558 ));
2559
2560 assert!(matches!(
2562 Opt::parse_including_static(["bootc", "status", "--verbose"]),
2563 Opt::Status(StatusOpts { verbose: true, .. })
2564 ));
2565
2566 assert!(matches!(
2568 Opt::parse_including_static(["bootc", "status", "-v"]),
2569 Opt::Status(StatusOpts { verbose: true, .. })
2570 ));
2571 }
2572
2573 #[test]
2574 fn test_parse_generator() {
2575 assert!(matches!(
2576 Opt::parse_including_static([
2577 "/usr/lib/systemd/system/bootc-systemd-generator",
2578 "/run/systemd/system"
2579 ]),
2580 Opt::Internals(InternalsOpts::SystemdGenerator { normal_dir, .. }) if normal_dir == "/run/systemd/system"
2581 ));
2582 }
2583
2584 #[test]
2585 fn test_parse_ostree_ext() {
2586 assert!(matches!(
2587 Opt::parse_including_static(["bootc", "internals", "ostree-container"]),
2588 Opt::Internals(InternalsOpts::OstreeContainer { .. })
2589 ));
2590
2591 fn peel(o: Opt) -> Vec<OsString> {
2592 match o {
2593 Opt::Internals(InternalsOpts::OstreeExt { args }) => args,
2594 o => panic!("unexpected {o:?}"),
2595 }
2596 }
2597 let args = peel(Opt::parse_including_static([
2598 "/usr/libexec/libostree/ext/ostree-ima-sign",
2599 "ima-sign",
2600 "--repo=foo",
2601 "foo",
2602 "bar",
2603 "baz",
2604 ]));
2605 assert_eq!(
2606 args.as_slice(),
2607 ["ima-sign", "--repo=foo", "foo", "bar", "baz"]
2608 );
2609
2610 let args = peel(Opt::parse_including_static([
2611 "/usr/libexec/libostree/ext/ostree-container",
2612 "container",
2613 "image",
2614 "pull",
2615 ]));
2616 assert_eq!(args.as_slice(), ["container", "image", "pull"]);
2617 }
2618
2619 #[test]
2620 fn test_parse_upgrade_options() {
2621 let o = Opt::try_parse_from(["bootc", "upgrade", "--tag", "v1.1"]).unwrap();
2623 match o {
2624 Opt::Upgrade(opts) => {
2625 assert_eq!(opts.tag, Some("v1.1".to_string()));
2626 }
2627 _ => panic!("Expected Upgrade variant"),
2628 }
2629
2630 let o = Opt::try_parse_from(["bootc", "upgrade", "--tag", "v1.1", "--check"]).unwrap();
2632 match o {
2633 Opt::Upgrade(opts) => {
2634 assert_eq!(opts.tag, Some("v1.1".to_string()));
2635 assert!(opts.check);
2636 }
2637 _ => panic!("Expected Upgrade variant"),
2638 }
2639 }
2640
2641 #[test]
2642 fn test_image_reference_with_tag() {
2643 let current = ImageReference {
2645 image: "quay.io/example/myapp:v1.0".to_string(),
2646 transport: "registry".to_string(),
2647 signature: None,
2648 };
2649 let result = current.with_tag("v1.1").unwrap();
2650 assert_eq!(result.image, "quay.io/example/myapp:v1.1");
2651 assert_eq!(result.transport, "registry");
2652
2653 let current_with_digest = ImageReference {
2655 image: "quay.io/example/myapp:v1.0@sha256:abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890".to_string(),
2656 transport: "registry".to_string(),
2657 signature: None,
2658 };
2659 let result = current_with_digest.with_tag("v2.0").unwrap();
2660 assert_eq!(result.image, "quay.io/example/myapp:v2.0");
2661
2662 let containers_storage = ImageReference {
2664 image: "localhost/myapp:v1.0".to_string(),
2665 transport: "containers-storage".to_string(),
2666 signature: None,
2667 };
2668 let result = containers_storage.with_tag("v1.1").unwrap();
2669 assert_eq!(result.image, "localhost/myapp:v1.1");
2670 assert_eq!(result.transport, "containers-storage");
2671
2672 let containers_storage_with_digest = ImageReference {
2674 image:
2675 "localhost/myapp:v1.0@sha256:abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890"
2676 .to_string(),
2677 transport: "containers-storage".to_string(),
2678 signature: None,
2679 };
2680 let result = containers_storage_with_digest.with_tag("v2.0").unwrap();
2681 assert_eq!(result.image, "localhost/myapp:v2.0");
2682 assert_eq!(result.transport, "containers-storage");
2683
2684 let no_tag = ImageReference {
2686 image: "localhost/myapp".to_string(),
2687 transport: "containers-storage".to_string(),
2688 signature: None,
2689 };
2690 let result = no_tag.with_tag("v1.0").unwrap();
2691 assert_eq!(result.image, "localhost/myapp:v1.0");
2692 assert_eq!(result.transport, "containers-storage");
2693 }
2694
2695 #[test]
2696 fn test_generate_completion_scripts_contain_commands() {
2697 use clap_complete::aot::{Shell, generate};
2698
2699 let want = ["install", "upgrade"];
2708
2709 for shell in [Shell::Bash, Shell::Zsh, Shell::Fish] {
2710 let mut cmd = Opt::command();
2711 let mut buf = Vec::new();
2712 generate(shell, &mut cmd, "bootc", &mut buf);
2713 let s = String::from_utf8(buf).expect("completion should be utf8");
2714 for w in &want {
2715 assert!(s.contains(w), "{shell:?} completion missing {w}");
2716 }
2717 }
2718 }
2719}