Skip to content

feat(edit): configure /usr overlay #1258

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 3 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 18 additions & 23 deletions lib/src/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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)]
Expand Down Expand Up @@ -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()?;

Expand All @@ -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
Expand Down
29 changes: 29 additions & 0 deletions lib/src/fixtures/spec-booted-usroverlay.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
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:
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
usrOverlay: readWrite
1 change: 1 addition & 0 deletions lib/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
15 changes: 15 additions & 0 deletions lib/src/mount.rs
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,21 @@ pub(crate) fn mount(dev: &str, target: &Utf8Path) -> Result<()> {
)
}

pub 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.
Expand Down
28 changes: 28 additions & 0 deletions lib/src/overlay.rs
Original file line number Diff line number Diff line change
@@ -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 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({});
}
43 changes: 42 additions & 1 deletion lib/src/spec.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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};

Expand Down Expand Up @@ -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<Self> {
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
Expand All @@ -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<FilesystemOverlay>,
}

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
Expand Down Expand Up @@ -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<FilesystemOverlay>,

/// The detected type of system
#[serde(rename = "type")]
Expand Down
62 changes: 55 additions & 7 deletions lib/src/status.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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};
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -260,6 +267,7 @@ pub(crate) fn get_status(
.map(|img| HostSpec {
image: Some(img.image.clone()),
boot_order,
usr_overlay,
})
.unwrap_or_default();

Expand All @@ -280,6 +288,7 @@ pub(crate) fn get_status(
booted,
rollback,
rollback_queued,
usr_overlay,
ty,
};
Ok((deployments, host))
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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(),
Expand All @@ -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(())
}

Expand All @@ -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")?;
}
Expand Down Expand Up @@ -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)
Expand All @@ -498,7 +532,7 @@ mod tests {
let expected = indoc::indoc! { r"
Staged ostree
Commit: 1c24260fdd1be20f72a4a97a75c582834ee3431fbb0fa8e4f482bb219d633a45

● Booted ostree
Commit: f9fa3a553ceaaaf30cf85bfe7eed46a822f7b8fd7e14c1e3389cbc3f6d27f791
"};
Expand All @@ -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
"};
Expand Down Expand Up @@ -578,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);
}
}
Loading