Skip to content

Commit

Permalink
Merge pull request #264 from cgwalters/switch-inplace
Browse files Browse the repository at this point in the history
Switch inplace
  • Loading branch information
cgwalters committed Jan 17, 2024
2 parents e5b5970 + 426af69 commit b75b647
Show file tree
Hide file tree
Showing 3 changed files with 174 additions and 16 deletions.
38 changes: 30 additions & 8 deletions lib/src/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

use anyhow::{Context, Result};
use camino::Utf8PathBuf;
use cap_std_ext::cap_std;
use clap::Parser;
use fn_error_context::context;
use ostree::gio;
Expand Down Expand Up @@ -71,6 +72,12 @@ pub(crate) struct SwitchOpts {
#[clap(long)]
pub(crate) ostree_remote: Option<String>,

/// Don't create a new deployment, but directly mutate the booted state.
/// This is hidden because it's not something we generally expect to be done,
/// but this can be used in e.g. Anaconda %post to fixup
#[clap(long, hide = true)]
pub(crate) mutate_in_place: bool,

/// Retain reference to currently booted image
#[clap(long)]
pub(crate) retain: bool,
Expand Down Expand Up @@ -386,14 +393,6 @@ async fn upgrade(opts: UpgradeOpts) -> Result<()> {
/// Implementation of the `bootc switch` CLI command.
#[context("Switching")]
async fn switch(opts: SwitchOpts) -> Result<()> {
prepare_for_write().await?;
let cancellable = gio::Cancellable::NONE;

let sysroot = &get_locked_sysroot().await?;
let repo = &sysroot.repo();
let (booted_deployment, _deployments, host) =
crate::status::get_status_require_booted(sysroot)?;

let transport = ostree_container::Transport::try_from(opts.transport.as_str())?;
let imgref = ostree_container::ImageReference {
transport,
Expand All @@ -406,6 +405,29 @@ async fn switch(opts: SwitchOpts) -> Result<()> {
let target = ostree_container::OstreeImageReference { sigverify, imgref };
let target = ImageReference::from(target);

// If we're doing an in-place mutation, we shortcut most of the rest of the work here
if opts.mutate_in_place {
let deployid = {
// Clone to pass into helper thread
let target = target.clone();
let root = cap_std::fs::Dir::open_ambient_dir("/", cap_std::ambient_authority())?;
tokio::task::spawn_blocking(move || {
crate::deploy::switch_origin_inplace(&root, &target)
})
.await??
};
println!("Updated {deployid} to pull from {target}");
return Ok(());
}

prepare_for_write().await?;
let cancellable = gio::Cancellable::NONE;

let sysroot = &get_locked_sysroot().await?;
let repo = &sysroot.repo();
let (booted_deployment, _deployments, host) =
crate::status::get_status_require_booted(sysroot)?;

let new_spec = {
let mut new_spec = host.spec.clone();
new_spec.image = Some(target.clone());
Expand Down
131 changes: 123 additions & 8 deletions lib/src/deploy.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,12 @@
//!
//! Create a merged filesystem tree with the image and mounted configmaps.

use anyhow::Ok;
use anyhow::{Context, Result};

use cap_std::fs::Dir;
use cap_std_ext::cap_std;
use cap_std_ext::dirext::CapStdExtDirExt;
use fn_error_context::context;
use ostree::{gio, glib};
use ostree_container::OstreeImageReference;
Expand All @@ -12,6 +16,7 @@ use ostree_ext::container::store::PrepareResult;
use ostree_ext::ostree;
use ostree_ext::ostree::Deployment;
use ostree_ext::sysroot::SysrootLock;
use rustix::fs::MetadataExt;

use crate::spec::HostSpec;
use crate::spec::ImageReference;
Expand Down Expand Up @@ -202,6 +207,18 @@ async fn deploy(
Ok(())
}

#[context("Generating origin")]
fn origin_from_imageref(imgref: &ImageReference) -> Result<glib::KeyFile> {
let origin = glib::KeyFile::new();
let imgref = OstreeImageReference::from(imgref.clone());
origin.set_string(
"origin",
ostree_container::deploy::ORIGIN_CONTAINER,
imgref.to_string().as_str(),
);
Ok(origin)
}

/// Stage (queue deployment of) a fetched container image.
#[context("Staging")]
pub(crate) async fn stage(
Expand All @@ -211,13 +228,7 @@ pub(crate) async fn stage(
spec: &RequiredHostSpec<'_>,
) -> Result<()> {
let merge_deployment = sysroot.merge_deployment(Some(stateroot));
let origin = glib::KeyFile::new();
let imgref = OstreeImageReference::from(spec.image.clone());
origin.set_string(
"origin",
ostree_container::deploy::ORIGIN_CONTAINER,
imgref.to_string().as_str(),
);
let origin = origin_from_imageref(spec.image)?;
crate::deploy::deploy(
sysroot,
merge_deployment.as_ref(),
Expand All @@ -227,11 +238,115 @@ pub(crate) async fn stage(
)
.await?;
crate::deploy::cleanup(sysroot).await?;
println!("Queued for next boot: {imgref}");
println!("Queued for next boot: {}", spec.image);
if let Some(version) = image.version.as_deref() {
println!(" Version: {version}");
}
println!(" Digest: {}", image.manifest_digest);

Ok(())
}

fn find_newest_deployment_name(deploysdir: &Dir) -> Result<String> {
let mut dirs = Vec::new();
for ent in deploysdir.entries()? {
let ent = ent?;
if !ent.file_type()?.is_dir() {
continue;
}
let name = ent.file_name();
let name = if let Some(name) = name.to_str() {
name
} else {
continue;
};
dirs.push((name.to_owned(), ent.metadata()?.mtime()));
}
dirs.sort_unstable_by(|a, b| a.1.cmp(&b.1));
if let Some((name, _ts)) = dirs.pop() {
Ok(name)
} else {
anyhow::bail!("No deployment directory found")
}
}

// Implementation of `bootc switch --in-place`
pub(crate) fn switch_origin_inplace(root: &Dir, imgref: &ImageReference) -> Result<String> {
// First, just create the new origin file
let origin = origin_from_imageref(imgref)?;
let serialized_origin = origin.to_data();

// Now, we can't rely on being officially booted (e.g. with the `ostree=` karg)
// in a scenario like running in the anaconda %post.
// Eventually, we should support a setup here where ostree-prepare-root
// can officially be run to "enter" an ostree root in a supportable way.
// Anyways for now, the brutal hack is to just scrape through the deployments
// and find the newest one, which we will mutate. If there's more than one,
// ultimately the calling tooling should be fixed to set things up correctly.

let mut ostree_deploys = root.open_dir("sysroot/ostree/deploy")?.entries()?;
let deploydir = loop {
if let Some(ent) = ostree_deploys.next() {
let ent = ent?;
if !ent.file_type()?.is_dir() {
continue;
}
tracing::debug!("Checking {:?}", ent.file_name());
let child_dir = ent
.open_dir()
.with_context(|| format!("Opening dir {:?}", ent.file_name()))?;
if let Some(d) = child_dir.open_dir_optional("deploy")? {
break d;
}
} else {
anyhow::bail!("Failed to find a deployment");
}
};
let newest_deployment = find_newest_deployment_name(&deploydir)?;
let origin_path = format!("{newest_deployment}.origin");
if !deploydir.try_exists(&origin_path)? {
tracing::warn!("No extant origin for {newest_deployment}");
}
deploydir
.atomic_write(&origin_path, serialized_origin.as_bytes())
.context("Writing origin")?;
return Ok(newest_deployment);
}

#[test]
fn test_switch_inplace() -> Result<()> {
use std::os::unix::fs::DirBuilderExt;

let td = cap_std_ext::cap_tempfile::TempDir::new(cap_std::ambient_authority())?;
let mut builder = cap_std::fs::DirBuilder::new();
let builder = builder.recursive(true).mode(0o755);
let deploydir = "sysroot/ostree/deploy/default/deploy";
let target_deployment = "af36eb0086bb55ac601600478c6168f834288013d60f8870b7851f44bf86c3c5.0";
td.ensure_dir_with(
format!("sysroot/ostree/deploy/default/deploy/{target_deployment}"),
builder,
)?;
let deploydir = &td.open_dir(deploydir)?;
let orig_imgref = ImageReference {
image: "quay.io/exampleos/original:sometag".into(),
transport: "registry".into(),
signature: None,
};
{
let origin = origin_from_imageref(&orig_imgref)?;
deploydir.atomic_write(
format!("{target_deployment}.origin"),
origin.to_data().as_bytes(),
)?;
}

let target_imgref = ImageReference {
image: "quay.io/someother/otherimage:latest".into(),
transport: "registry".into(),
signature: None,
};

let replaced = switch_origin_inplace(&td, &target_imgref).unwrap();
assert_eq!(replaced, target_deployment);
Ok(())
}
21 changes: 21 additions & 0 deletions lib/src/spec.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
//! The definition for host system state.

use std::fmt::Display;

use ostree_ext::container::OstreeImageReference;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};

Expand Down Expand Up @@ -149,8 +152,17 @@ impl Default for Host {
}
}

impl Display for ImageReference {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let ostree_imgref = OstreeImageReference::from(self.clone());
ostree_imgref.fmt(f)
}
}

#[cfg(test)]
mod tests {
use std::str::FromStr;

use super::*;

#[test]
Expand Down Expand Up @@ -183,4 +195,13 @@ mod tests {
Some(ImageSignature::OstreeRemote("fedora".into()))
);
}

#[test]
fn test_display_imgref() {
let src = "ostree-unverified-registry:quay.io/example/foo:sometag";
let s = OstreeImageReference::from_str(src).unwrap();
let s = ImageReference::from(s);
let displayed = format!("{s}");
assert_eq!(displayed.as_str(), src);
}
}

0 comments on commit b75b647

Please sign in to comment.