Skip to content

Commit

Permalink
Merge pull request #447 from cgwalters/add-rollback
Browse files Browse the repository at this point in the history
Add a `rollback` verb and `rollbackQueued` status
  • Loading branch information
jeckersb committed Mar 27, 2024
2 parents 1f8d03c + 04baad3 commit 1707bdd
Show file tree
Hide file tree
Showing 5 changed files with 148 additions and 5 deletions.
33 changes: 33 additions & 0 deletions lib/src/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,10 @@ pub(crate) struct SwitchOpts {
pub(crate) target: String,
}

/// Options controlling rollback
#[derive(Debug, Parser, PartialEq, Eq)]
pub(crate) struct RollbackOpts {}

/// Perform an edit operation
#[derive(Debug, Parser, PartialEq, Eq)]
pub(crate) struct EditOpts {
Expand Down Expand Up @@ -214,6 +218,18 @@ pub(crate) enum Opt {
/// This operates in a very similar fashion to `upgrade`, but changes the container image reference
/// instead.
Switch(SwitchOpts),
/// Change the bootloader entry ordering; the deployment under `rollback` will be queued for the next boot,
/// and the current will become rollback. If there is a `staged` entry (an unapplied, queued upgrade)
/// then it will be discarded.
///
/// Note that absent any additional control logic, if there is an active agent doing automated upgrades
/// (such as the default `bootc-fetch-apply-updates.timer` and associated `.service`) the
/// change here may be reverted. It's recommended to only use this in concert with an agent that
/// is in active control.
///
/// A systemd journal message will be logged with `MESSAGE_ID=26f3b1eb24464d12aa5e7b544a6b5468` in
/// order to detect a rollback invocation.
Rollback(RollbackOpts),
/// Apply full changes to the host specification.
///
/// This command operates very similarly to `kubectl apply`; if invoked interactively,
Expand Down Expand Up @@ -500,6 +516,14 @@ async fn switch(opts: SwitchOpts) -> Result<()> {
Ok(())
}

/// Implementation of the `bootc rollback` CLI command.
#[context("Rollback")]
async fn rollback(_opts: RollbackOpts) -> Result<()> {
prepare_for_write().await?;
let sysroot = &get_locked_sysroot().await?;
crate::deploy::rollback(sysroot).await
}

/// Implementation of the `bootc edit` CLI command.
#[context("Editing spec")]
async fn edit(opts: EditOpts) -> Result<()> {
Expand All @@ -522,7 +546,15 @@ async fn edit(opts: EditOpts) -> Result<()> {
println!("Edit cancelled, no changes made.");
return Ok(());
}
host.spec.verify_transition(&new_host.spec)?;
let new_spec = RequiredHostSpec::from_spec(&new_host.spec)?;

// 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;
}

let fetched = crate::deploy::pull(sysroot, new_spec.image, opts.quiet).await?;

// TODO gc old layers here
Expand Down Expand Up @@ -586,6 +618,7 @@ async fn run_from_opt(opt: Opt) -> Result<()> {
match opt {
Opt::Upgrade(opts) => upgrade(opts).await,
Opt::Switch(opts) => switch(opts).await,
Opt::Rollback(opts) => rollback(opts).await,
Opt::Edit(opts) => edit(opts).await,
Opt::UsrOverlay => usroverlay().await,
#[cfg(feature = "install")]
Expand Down
61 changes: 59 additions & 2 deletions lib/src/deploy.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
use std::io::{BufRead, Write};

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

use cap_std::fs::{Dir, MetadataExt};
use cap_std_ext::cap_std;
Expand All @@ -19,8 +19,8 @@ use ostree_ext::ostree;
use ostree_ext::ostree::Deployment;
use ostree_ext::sysroot::SysrootLock;

use crate::spec::HostSpec;
use crate::spec::ImageReference;
use crate::spec::{BootOrder, HostSpec};
use crate::status::labels_of_config;

// TODO use https://github.com/ostreedev/ostree-rs-ext/pull/493/commits/afc1837ff383681b947de30c0cefc70080a4f87a
Expand Down Expand Up @@ -276,6 +276,63 @@ pub(crate) async fn stage(
Ok(())
}

/// Implementation of rollback functionality
pub(crate) async fn rollback(sysroot: &SysrootLock) -> Result<()> {
const ROLLBACK_JOURNAL_ID: &str = "26f3b1eb24464d12aa5e7b544a6b5468";
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.boot_order = new_spec.boot_order.swap();
new_spec
};

// Just to be sure
host.spec.verify_transition(&new_spec)?;

let reverting = new_spec.boot_order == BootOrder::Default;
if reverting {
println!("notice: Reverting queued rollback state");
}
let rollback_status = host
.status
.rollback
.ok_or_else(|| anyhow!("No rollback available"))?;
let rollback_image = rollback_status
.query_image(repo)?
.ok_or_else(|| anyhow!("Rollback is not container image based"))?;
let msg = format!("Rolling back to image: {}", rollback_image.manifest_digest);
libsystemd::logging::journal_send(
libsystemd::logging::Priority::Info,
&msg,
[
("MESSAGE_ID", ROLLBACK_JOURNAL_ID),
("BOOTC_MANIFEST_DIGEST", &rollback_image.manifest_digest),
]
.into_iter(),
)?;
// SAFETY: If there's a rollback status, then there's a deployment
let rollback_deployment = deployments.rollback.expect("rollback deployment");
let new_deployments = if reverting {
[booted_deployment, rollback_deployment]
} else {
[rollback_deployment, booted_deployment]
};
let new_deployments = new_deployments
.into_iter()
.chain(deployments.other)
.collect::<Vec<_>>();
tracing::debug!("Writing new deployments: {new_deployments:?}");
sysroot.write_deployments(&new_deployments, gio::Cancellable::NONE)?;
if reverting {
println!("Next boot: current deployment");
} else {
println!("Next boot: rollback deployment");
}
Ok(())
}

fn find_newest_deployment_name(deploysdir: &Dir) -> Result<String> {
let mut dirs = Vec::new();
for ent in deploysdir.entries()? {
Expand Down
40 changes: 40 additions & 0 deletions lib/src/spec.rs
Original file line number Diff line number Diff line change
Expand Up @@ -28,12 +28,27 @@ pub struct Host {
pub status: HostStatus,
}

/// Configuration for system boot ordering.

#[derive(Serialize, Deserialize, Default, Debug, Clone, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub enum BootOrder {
/// The staged or booted deployment will be booted next
#[default]
Default,
/// The rollback deployment will be booted next
Rollback,
}

#[derive(Serialize, Deserialize, Default, Debug, Clone, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
/// The host specification
pub struct HostSpec {
/// The host image
pub image: Option<ImageReference>,
/// If set, and there is a rollback deployment, it will be set for the next boot.
#[serde(default)]
pub boot_order: BootOrder,
}

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
Expand Down Expand Up @@ -121,6 +136,9 @@ pub struct HostStatus {
pub booted: Option<BootEntry>,
/// The previously booted image
pub rollback: Option<BootEntry>,
/// Set to true if the rollback entry is queued for the next boot.
#[serde(default)]
pub rollback_queued: bool,

/// The detected type of system
#[serde(rename = "type")]
Expand Down Expand Up @@ -152,6 +170,28 @@ impl Default for Host {
}
}

impl HostSpec {
/// Validate a spec state transition; some changes cannot be made simultaneously,
/// such as fetching a new image and doing a rollback.
pub(crate) fn verify_transition(&self, new: &Self) -> anyhow::Result<()> {
let rollback = self.boot_order != new.boot_order;
let image_change = self.image != new.image;
if rollback && image_change {
anyhow::bail!("Invalid state transition: rollback and image change");
}
Ok(())
}
}

impl BootOrder {
pub(crate) fn swap(&self) -> Self {
match self {
BootOrder::Default => BootOrder::Rollback,
BootOrder::Rollback => BootOrder::Default,
}
}
}

impl Display for ImageReference {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
// For the default of fetching from a remote registry, just output the image name
Expand Down
15 changes: 14 additions & 1 deletion lib/src/status.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
use std::collections::VecDeque;

use crate::spec::{BootEntry, Host, HostSpec, HostStatus, HostType, ImageStatus};
use crate::spec::{BootEntry, BootOrder, Host, HostSpec, HostStatus, HostType, ImageStatus};
use crate::spec::{ImageReference, ImageSignature};
use anyhow::{Context, Result};
use camino::Utf8Path;
Expand Down Expand Up @@ -224,11 +224,22 @@ pub(crate) fn get_status(
.iter()
.position(|d| d.is_staged())
.map(|i| related_deployments.remove(i).unwrap());
tracing::debug!("Staged: {staged:?}");
// Filter out the booted, the caller already found that
if let Some(booted) = booted_deployment.as_ref() {
related_deployments.retain(|f| !f.equal(booted));
}
let rollback = related_deployments.pop_front();
let rollback_queued = match (booted_deployment.as_ref(), rollback.as_ref()) {
(Some(booted), Some(rollback)) => rollback.index() < booted.index(),
_ => false,
};
let boot_order = if rollback_queued {
BootOrder::Rollback
} else {
BootOrder::Default
};
tracing::debug!("Rollback queued={rollback_queued:?}");
let other = {
related_deployments.extend(other_deployments);
related_deployments
Expand Down Expand Up @@ -262,6 +273,7 @@ pub(crate) fn get_status(
.and_then(|entry| entry.image.as_ref())
.map(|img| HostSpec {
image: Some(img.image.clone()),
boot_order,
})
.unwrap_or_default();

Expand All @@ -281,6 +293,7 @@ pub(crate) fn get_status(
staged,
booted,
rollback,
rollback_queued,
ty,
};
Ok((deployments, host))
Expand Down
4 changes: 2 additions & 2 deletions tests/integration/playbooks/rollback.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@
failed_counter: "0"

tasks:
- name: rpm-ostree rollback
command: rpm-ostree rollback
- name: bootc rollback
command: bootc rollback
become: true

- name: Reboot to deploy new system
Expand Down

0 comments on commit 1707bdd

Please sign in to comment.