Skip to content
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

Switch inplace #264

Merged
merged 3 commits into from
Jan 17, 2024
Merged
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
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);
}
}