From 82317e6fd734ba01e72041aa2b4dd0891574fdc4 Mon Sep 17 00:00:00 2001 From: Robert Sturla Date: Tue, 8 Apr 2025 23:07:00 +0100 Subject: [PATCH 1/3] feat(status): display usroverlay state in status Closes #474 Displays the status of the usroverlay within bootc status. Although not ideal, handles the displaying of the host's filesystem within the image/ostree functions since otherwise it may require the introduction of another "host" block within the status output (i.e. having a "host", "staged", "booted" and "rollback"). Signed-off-by: Robert Sturla --- lib/src/spec.rs | 43 +++++++++++++++++++++++++++++++++++++++++- lib/src/status.rs | 48 ++++++++++++++++++++++++++++++++++++++++------- 2 files changed, 83 insertions(+), 8 deletions(-) diff --git a/lib/src/spec.rs b/lib/src/spec.rs index fb784570..3a5e0634 100644 --- a/lib/src/spec.rs +++ b/lib/src/spec.rs @@ -2,8 +2,8 @@ use std::fmt::Display; -use ostree_ext::container::OstreeImageReference; use ostree_ext::oci_spec::image::Digest; +use ostree_ext::{container::OstreeImageReference, ostree::DeploymentUnlockedState}; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; @@ -53,6 +53,41 @@ pub enum Store { OstreeContainer, } +#[derive( + clap::ValueEnum, Serialize, Deserialize, Copy, Clone, Debug, PartialEq, Eq, JsonSchema, +)] +#[serde(rename_all = "camelCase")] +/// The filesystem overlay type +pub enum FilesystemOverlay { + /// Readonly overlay mode + Readonly, + /// Read-write overlay mode + ReadWrite, +} + +impl FilesystemOverlay { + /// Convert from the ostree deployment state + pub fn from_ostree_deployment_state(state: &DeploymentUnlockedState) -> Option { + match state { + DeploymentUnlockedState::None => Some(Self::Readonly), + DeploymentUnlockedState::Development + | DeploymentUnlockedState::Transient + | DeploymentUnlockedState::Hotfix => Some(Self::ReadWrite), + // Default to readonly when unknown since it is the default + // for bootc deployments. + _ => Some(Self::Readonly), + } + } + + /// Convert the FilesystemOverlay value to a human-readable string + pub fn to_human_string(&self) -> String { + match self { + Self::Readonly => "read-only".to_string(), + Self::ReadWrite => "read-write".to_string(), + } + } +} + #[derive(Serialize, Deserialize, Default, Debug, Clone, PartialEq, Eq, JsonSchema)] #[serde(rename_all = "camelCase")] /// The host specification @@ -62,6 +97,9 @@ pub struct HostSpec { /// If set, and there is a rollback deployment, it will be set for the next boot. #[serde(default)] pub boot_order: BootOrder, + /// Matches the `ostree admin unlock` state + #[serde(default)] + pub usr_overlay: Option, } #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, JsonSchema)] @@ -157,6 +195,9 @@ pub struct HostStatus { /// Set to true if the rollback entry is queued for the next boot. #[serde(default)] pub rollback_queued: bool, + /// Matches the `ostree admin unlock` state + #[serde(default)] + pub usr_overlay: Option, /// The detected type of system #[serde(rename = "type")] diff --git a/lib/src/status.rs b/lib/src/status.rs index 5bc4d348..433777bb 100644 --- a/lib/src/status.rs +++ b/lib/src/status.rs @@ -15,6 +15,7 @@ use ostree_ext::oci_spec; use ostree_ext::ostree; use crate::cli::OutputFormat; +use crate::spec::FilesystemOverlay; use crate::spec::{BootEntry, BootOrder, Host, HostSpec, HostStatus, HostType}; use crate::spec::{ImageReference, ImageSignature}; use crate::store::{CachedImageStatus, ContainerImageStore, Storage}; @@ -225,6 +226,12 @@ pub(crate) fn get_status( } else { BootOrder::Default }; + let usr_overlay = FilesystemOverlay::from_ostree_deployment_state( + &booted_deployment + .as_ref() + .expect("Expected a booted deployment") + .unlocked(), + ); tracing::debug!("Rollback queued={rollback_queued:?}"); let other = { related_deployments.extend(other_deployments); @@ -260,6 +267,7 @@ pub(crate) fn get_status( .map(|img| HostSpec { image: Some(img.image.clone()), boot_order, + usr_overlay, }) .unwrap_or_default(); @@ -280,6 +288,7 @@ pub(crate) fn get_status( booted, rollback, rollback_queued, + usr_overlay, ty, }; Ok((deployments, host)) @@ -331,7 +340,7 @@ pub(crate) async fn status(opts: super::cli::StatusOpts) -> Result<()> { Ok(()) } -#[derive(Debug)] +#[derive(Debug, PartialEq)] pub enum Slot { Staged, Booted, @@ -363,6 +372,7 @@ fn human_render_imagestatus( mut out: impl Write, slot: Slot, image: &crate::spec::ImageStatus, + host: &crate::spec::HostStatus, ) -> Result<()> { let transport = &image.image.transport; let imagename = &image.image.image; @@ -407,10 +417,24 @@ fn human_render_imagestatus( writeln!(out, "{timestamp}")?; } + if let Some(usr_overlay) = &host.usr_overlay { + // Only show the usr filesystem overlay state if we are booted and not the default + // read-only mode. + if slot == Slot::Booted && *usr_overlay != FilesystemOverlay::Readonly { + write_row_name(&mut out, "Usr overlay", prefix_len)?; + writeln!(out, "{}", usr_overlay.to_human_string())?; + } + } + Ok(()) } -fn human_render_ostree(mut out: impl Write, slot: Slot, ostree_commit: &str) -> Result<()> { +fn human_render_ostree( + mut out: impl Write, + slot: Slot, + ostree_commit: &str, + host: &crate::spec::HostStatus, +) -> Result<()> { // TODO consider rendering more ostree stuff here like rpm-ostree status does let prefix = match slot { Slot::Staged => " Staged ostree".into(), @@ -421,6 +445,16 @@ fn human_render_ostree(mut out: impl Write, slot: Slot, ostree_commit: &str) -> writeln!(out, "{prefix}")?; write_row_name(&mut out, "Commit", prefix_len)?; writeln!(out, "{ostree_commit}")?; + + if let Some(usr_overlay) = &host.usr_overlay { + // Only show the usr filesystem overlay state if we are booted and not the default + // read-only mode. + if slot == Slot::Booted && *usr_overlay != FilesystemOverlay::Readonly { + write_row_name(&mut out, "Usr overlay", prefix_len)?; + writeln!(out, "{}", usr_overlay.to_human_string())?; + } + } + Ok(()) } @@ -438,9 +472,9 @@ fn human_readable_output_booted(mut out: impl Write, host: &Host) -> Result<()> writeln!(out)?; } if let Some(image) = &host_status.image { - human_render_imagestatus(&mut out, slot_name, image)?; + human_render_imagestatus(&mut out, slot_name, image, &host.status)?; } else if let Some(ostree) = host_status.ostree.as_ref() { - human_render_ostree(&mut out, slot_name, &ostree.checksum)?; + human_render_ostree(&mut out, slot_name, &ostree.checksum, &host.status)?; } else { writeln!(out, "Current {slot_name} state is unknown")?; } @@ -480,7 +514,7 @@ mod tests { Staged image: quay.io/example/someimage:latest Digest: sha256:16dc2b6256b4ff0d2ec18d2dbfb06d117904010c8cf9732cdb022818cf7a7566 (arm64) Version: nightly (2023-10-14T19:22:15Z) - + ● Booted image: quay.io/example/someimage:latest Digest: sha256:736b359467c9437c1ac915acaae952aad854e07eb4a16a94999a48af08c83c34 (arm64) Version: nightly (2023-09-30T19:22:16Z) @@ -498,7 +532,7 @@ mod tests { let expected = indoc::indoc! { r" Staged ostree Commit: 1c24260fdd1be20f72a4a97a75c582834ee3431fbb0fa8e4f482bb219d633a45 - + ● Booted ostree Commit: f9fa3a553ceaaaf30cf85bfe7eed46a822f7b8fd7e14c1e3389cbc3f6d27f791 "}; @@ -514,7 +548,7 @@ mod tests { Staged image: quay.io/centos-bootc/centos-bootc:stream9 Digest: sha256:47e5ed613a970b6574bfa954ab25bb6e85656552899aa518b5961d9645102b38 (s390x) Version: stream9.20240807.0 - + ● Booted ostree Commit: f9fa3a553ceaaaf30cf85bfe7eed46a822f7b8fd7e14c1e3389cbc3f6d27f791 "}; From 0e04f5a95a31eb5e2f24b65dbd20ea781610ba25 Mon Sep 17 00:00:00 2001 From: Robert Sturla Date: Sat, 26 Apr 2025 20:15:34 +0100 Subject: [PATCH 2/3] feat(edit): allow setting usrOverlay Allows setting the usrOverlay within "bootc edit". Since there's no clean ways (that I'm aware of) for switching back from a writable /usr to a readonly one, this follows the recommended commands in the docs, and lazily unmounts /usr. Signed-off-by: Robert Sturla --- lib/src/cli.rs | 41 ++++++++--------- .../spec-staged-booted-usrunlock.yaml | 44 +++++++++++++++++++ lib/src/lib.rs | 1 + lib/src/mount.rs | 15 +++++++ lib/src/overlay.rs | 28 ++++++++++++ 5 files changed, 106 insertions(+), 23 deletions(-) create mode 100644 lib/src/fixtures/spec-staged-booted-usrunlock.yaml create mode 100644 lib/src/overlay.rs diff --git a/lib/src/cli.rs b/lib/src/cli.rs index cc3fa808..05fe1b72 100644 --- a/lib/src/cli.rs +++ b/lib/src/cli.rs @@ -4,8 +4,6 @@ use std::ffi::{CString, OsStr, OsString}; use std::io::Seek; -use std::os::unix::process::CommandExt; -use std::process::Command; use anyhow::{ensure, Context, Result}; use camino::Utf8PathBuf; @@ -26,11 +24,11 @@ use schemars::schema_for; use serde::{Deserialize, Serialize}; use crate::deploy::RequiredHostSpec; -use crate::lints; use crate::progress_jsonl::{ProgressWriter, RawProgressFd}; -use crate::spec::Host; use crate::spec::ImageReference; +use crate::spec::{FilesystemOverlay, Host}; use crate::utils::sigpolicy_from_opt; +use crate::{lints, overlay}; /// Shared progress options #[derive(Debug, Parser, PartialEq, Eq)] @@ -971,20 +969,22 @@ async fn edit(opts: EditOpts) -> Result<()> { host.spec.verify_transition(&new_host.spec)?; let new_spec = RequiredHostSpec::from_spec(&new_host.spec)?; - let prog = ProgressWriter::default(); - - // We only support two state transitions right now; switching the image, - // or flipping the bootloader ordering. - if host.spec.boot_order != new_host.spec.boot_order { - return crate::deploy::rollback(sysroot).await; + if new_host.spec.boot_order != host.spec.boot_order { + crate::deploy::rollback(sysroot).await?; + } + if new_host.spec.image != host.spec.image { + let prog = ProgressWriter::default(); + let fetched = + crate::deploy::pull(repo, new_spec.image, None, opts.quiet, prog.clone()).await?; + // TODO gc old layers here + let stateroot = booted_deployment.osname(); + crate::deploy::stage(sysroot, &stateroot, &fetched, &new_spec, prog.clone()).await?; + } + if new_host.spec.usr_overlay != host.spec.usr_overlay { + if let Some(overlay) = new_host.spec.usr_overlay { + crate::overlay::set_usr_overlay(overlay)?; + } } - - let fetched = crate::deploy::pull(repo, new_spec.image, None, opts.quiet, prog.clone()).await?; - - // TODO gc old layers here - - let stateroot = booted_deployment.osname(); - crate::deploy::stage(sysroot, &stateroot, &fetched, &new_spec, prog.clone()).await?; sysroot.update_mtime()?; @@ -993,12 +993,7 @@ async fn edit(opts: EditOpts) -> Result<()> { /// Implementation of `bootc usroverlay` async fn usroverlay() -> Result<()> { - // This is just a pass-through today. At some point we may make this a libostree API - // or even oxidize it. - Err(Command::new("ostree") - .args(["admin", "unlock"]) - .exec() - .into()) + overlay::set_usr_overlay(FilesystemOverlay::ReadWrite) } /// Perform process global initialization. This should be called as early as possible diff --git a/lib/src/fixtures/spec-staged-booted-usrunlock.yaml b/lib/src/fixtures/spec-staged-booted-usrunlock.yaml new file mode 100644 index 00000000..72e114e9 --- /dev/null +++ b/lib/src/fixtures/spec-staged-booted-usrunlock.yaml @@ -0,0 +1,44 @@ +apiVersion: org.containers.bootc/v1alpha1 +kind: BootcHost +metadata: + name: host +spec: + image: + image: quay.io/example/someimage:latest + transport: registry + signature: insecure + usrOverlay: readWrite +status: + staged: + image: + image: + image: quay.io/example/someimage:latest + transport: registry + signature: insecure + architecture: arm64 + version: nightly + # This one has nanoseconds, which should be dropped for human consumption + timestamp: 2023-10-14T19:22:15.42Z + imageDigest: sha256:16dc2b6256b4ff0d2ec18d2dbfb06d117904010c8cf9732cdb022818cf7a7566 + incompatible: false + pinned: false + ostree: + checksum: 3c6dad657109522e0b2e49bf44b5420f16f0b438b5b9357e5132211cfbad135d + deploySerial: 0 + booted: + image: + image: + image: quay.io/example/someimage:latest + transport: registry + signature: insecure + architecture: arm64 + version: nightly + timestamp: 2023-09-30T19:22:16Z + imageDigest: sha256:736b359467c9437c1ac915acaae952aad854e07eb4a16a94999a48af08c83c34 + incompatible: false + pinned: false + ostree: + checksum: 26836632adf6228d64ef07a26fd3efaf177104efd1f341a2cf7909a3e4e2c72c + deploySerial: 0 + rollback: null + isContainer: false diff --git a/lib/src/lib.rs b/lib/src/lib.rs index d05e9f96..dad4e1b2 100644 --- a/lib/src/lib.rs +++ b/lib/src/lib.rs @@ -27,6 +27,7 @@ mod status; mod store; mod task; mod utils; +mod overlay; #[cfg(feature = "docgen")] mod docgen; diff --git a/lib/src/mount.rs b/lib/src/mount.rs index 5aa2ef2c..d74219a3 100644 --- a/lib/src/mount.rs +++ b/lib/src/mount.rs @@ -129,6 +129,21 @@ pub(crate) fn mount(dev: &str, target: &Utf8Path) -> Result<()> { ) } +pub(crate) fn unmount(target: &Utf8Path, lazy: bool) -> Result<()> { + let args = if lazy { + vec!["-l", target.as_str()] + } else { + vec![target.as_str()] + }; + + let description = if lazy { + format!("Lazily unmounting {target}.\nThis may leave lingering mounts if in use") + } else { + format!("Unmounting {target}") + }; + Task::new_and_run(description, "umount", args) +} + /// If the fsid of the passed path matches the fsid of the same path rooted /// at /proc/1/root, it is assumed that these are indeed the same mounted /// filesystem between container and host. diff --git a/lib/src/overlay.rs b/lib/src/overlay.rs new file mode 100644 index 00000000..0d6e836e --- /dev/null +++ b/lib/src/overlay.rs @@ -0,0 +1,28 @@ +//! Handling of deployment overlays + +use std::{os::unix::process::CommandExt, process::Command}; + +use anyhow::Result; +use fn_error_context::context; + +use crate::spec; + +#[context("Setting /usr overlay")] +pub(crate) fn set_usr_overlay(state: spec::FilesystemOverlay) -> Result<()> { + match state { + spec::FilesystemOverlay::Readonly => { + tracing::info!("Setting /usr overlay to read-only"); + // There's no clean way to remove the readwrite overlay, so we lazily unmount it. + crate::mount::unmount(camino::Utf8Path::new("/usr"), true)?; + } + spec::FilesystemOverlay::ReadWrite => { + tracing::info!("Setting /usr overlay to read-write"); + // This is just a pass-through today. At some point we may make this a libostree API + // or even oxidize it. + Err(anyhow::Error::from( + Command::new("ostree").args(["admin", "unlock"]).exec(), + ))?; + } + } + return Ok({}); +} From 07509f6a42f2ab258575ddcd33ca640e02d57608 Mon Sep 17 00:00:00 2001 From: Robert Sturla Date: Sat, 26 Apr 2025 20:34:36 +0100 Subject: [PATCH 3/3] chore(tests): implement /usr overlay tests Signed-off-by: Robert Sturla --- ...unlock.yaml => spec-booted-usroverlay.yaml} | 17 +---------------- lib/src/lib.rs | 2 +- lib/src/mount.rs | 2 +- lib/src/overlay.rs | 2 +- lib/src/status.rs | 18 ++++++++++++++++-- 5 files changed, 20 insertions(+), 21 deletions(-) rename lib/src/fixtures/{spec-staged-booted-usrunlock.yaml => spec-booted-usroverlay.yaml} (56%) diff --git a/lib/src/fixtures/spec-staged-booted-usrunlock.yaml b/lib/src/fixtures/spec-booted-usroverlay.yaml similarity index 56% rename from lib/src/fixtures/spec-staged-booted-usrunlock.yaml rename to lib/src/fixtures/spec-booted-usroverlay.yaml index 72e114e9..3392a295 100644 --- a/lib/src/fixtures/spec-staged-booted-usrunlock.yaml +++ b/lib/src/fixtures/spec-booted-usroverlay.yaml @@ -9,22 +9,6 @@ spec: signature: insecure usrOverlay: readWrite status: - staged: - image: - image: - image: quay.io/example/someimage:latest - transport: registry - signature: insecure - architecture: arm64 - version: nightly - # This one has nanoseconds, which should be dropped for human consumption - timestamp: 2023-10-14T19:22:15.42Z - imageDigest: sha256:16dc2b6256b4ff0d2ec18d2dbfb06d117904010c8cf9732cdb022818cf7a7566 - incompatible: false - pinned: false - ostree: - checksum: 3c6dad657109522e0b2e49bf44b5420f16f0b438b5b9357e5132211cfbad135d - deploySerial: 0 booted: image: image: @@ -42,3 +26,4 @@ status: deploySerial: 0 rollback: null isContainer: false + usrOverlay: readWrite diff --git a/lib/src/lib.rs b/lib/src/lib.rs index dad4e1b2..16705b20 100644 --- a/lib/src/lib.rs +++ b/lib/src/lib.rs @@ -18,6 +18,7 @@ pub(crate) mod kargs; mod lints; mod lsm; pub(crate) mod metadata; +mod overlay; mod podman; mod progress_jsonl; mod reboot; @@ -27,7 +28,6 @@ mod status; mod store; mod task; mod utils; -mod overlay; #[cfg(feature = "docgen")] mod docgen; diff --git a/lib/src/mount.rs b/lib/src/mount.rs index d74219a3..d8425c65 100644 --- a/lib/src/mount.rs +++ b/lib/src/mount.rs @@ -129,7 +129,7 @@ pub(crate) fn mount(dev: &str, target: &Utf8Path) -> Result<()> { ) } -pub(crate) fn unmount(target: &Utf8Path, lazy: bool) -> Result<()> { +pub fn unmount(target: &Utf8Path, lazy: bool) -> Result<()> { let args = if lazy { vec!["-l", target.as_str()] } else { diff --git a/lib/src/overlay.rs b/lib/src/overlay.rs index 0d6e836e..7f295bda 100644 --- a/lib/src/overlay.rs +++ b/lib/src/overlay.rs @@ -8,7 +8,7 @@ use fn_error_context::context; use crate::spec; #[context("Setting /usr overlay")] -pub(crate) fn set_usr_overlay(state: spec::FilesystemOverlay) -> Result<()> { +pub fn set_usr_overlay(state: spec::FilesystemOverlay) -> Result<()> { match state { spec::FilesystemOverlay::Readonly => { tracing::info!("Setting /usr overlay to read-only"); diff --git a/lib/src/status.rs b/lib/src/status.rs index 433777bb..43f46675 100644 --- a/lib/src/status.rs +++ b/lib/src/status.rs @@ -421,7 +421,7 @@ fn human_render_imagestatus( // Only show the usr filesystem overlay state if we are booted and not the default // read-only mode. if slot == Slot::Booted && *usr_overlay != FilesystemOverlay::Readonly { - write_row_name(&mut out, "Usr overlay", prefix_len)?; + write_row_name(&mut out, "/usr overlay", prefix_len)?; writeln!(out, "{}", usr_overlay.to_human_string())?; } } @@ -450,7 +450,7 @@ fn human_render_ostree( // Only show the usr filesystem overlay state if we are booted and not the default // read-only mode. if slot == Slot::Booted && *usr_overlay != FilesystemOverlay::Readonly { - write_row_name(&mut out, "Usr overlay", prefix_len)?; + write_row_name(&mut out, "/usr overlay", prefix_len)?; writeln!(out, "{}", usr_overlay.to_human_string())?; } } @@ -612,4 +612,18 @@ mod tests { Some(ImageSignature::OstreeRemote("fedora".into())) ); } + + #[test] + fn test_human_readable_booted_usroverlay() { + let w = + human_status_from_spec_fixture(include_str!("fixtures/spec-booted-usroverlay.yaml")) + .unwrap(); + let expected = indoc::indoc! { r" + ● Booted image: quay.io/example/someimage:latest + Digest: sha256:736b359467c9437c1ac915acaae952aad854e07eb4a16a94999a48af08c83c34 (arm64) + Version: nightly (2023-09-30T19:22:16Z) + /usr overlay: read-write + "}; + similar_asserts::assert_eq!(w, expected); + } }