Skip to content

Commit

Permalink
Add a rollback verb and rollbackQueued status
Browse files Browse the repository at this point in the history
I'd really hoped to do something more declarative here, and
really flesh out the intersections with automated upgrades
and automated rollbacks.

But, this just exposes the simple primitive, equivalent
to `rpm-ostree rollback`.

Signed-off-by: Colin Walters <[email protected]>
  • Loading branch information
cgwalters committed Mar 26, 2024
1 parent fd94c6f commit 04baad3
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 04baad3

Please sign in to comment.