summaryrefslogtreecommitdiff
path: root/0001-Add-a-rollback-verb-and-rollbackQueued-status.patch
diff options
context:
space:
mode:
Diffstat (limited to '0001-Add-a-rollback-verb-and-rollbackQueued-status.patch')
-rw-r--r--0001-Add-a-rollback-verb-and-rollbackQueued-status.patch321
1 files changed, 321 insertions, 0 deletions
diff --git a/0001-Add-a-rollback-verb-and-rollbackQueued-status.patch b/0001-Add-a-rollback-verb-and-rollbackQueued-status.patch
new file mode 100644
index 0000000..b96ac6c
--- /dev/null
+++ b/0001-Add-a-rollback-verb-and-rollbackQueued-status.patch
@@ -0,0 +1,321 @@
+From 04baad3080246b1ce26df7a783874b2b1b3ddaa4 Mon Sep 17 00:00:00 2001
+From: Colin Walters <walters@verbum.org>
+Date: Sat, 9 Mar 2024 15:33:27 -0500
+Subject: [PATCH] Add a `rollback` verb and `rollbackQueued` status
+
+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 <walters@verbum.org>
+---
+ lib/src/cli.rs | 33 ++++++++++++
+ lib/src/deploy.rs | 61 ++++++++++++++++++++++-
+ lib/src/spec.rs | 40 +++++++++++++++
+ lib/src/status.rs | 15 +++++-
+ tests/integration/playbooks/rollback.yaml | 4 +-
+ 5 files changed, 148 insertions(+), 5 deletions(-)
+
+diff --git a/lib/src/cli.rs b/lib/src/cli.rs
+index 42bfbcc..623796a 100644
+--- a/lib/src/cli.rs
++++ b/lib/src/cli.rs
+@@ -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 {
+@@ -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,
+@@ -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<()> {
+@@ -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
+@@ -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")]
+diff --git a/lib/src/deploy.rs b/lib/src/deploy.rs
+index 444ad8e..3eb31a8 100644
+--- a/lib/src/deploy.rs
++++ b/lib/src/deploy.rs
+@@ -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;
+@@ -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
+@@ -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()? {
+diff --git a/lib/src/spec.rs b/lib/src/spec.rs
+index 6de9639..5f6df93 100644
+--- a/lib/src/spec.rs
++++ b/lib/src/spec.rs
+@@ -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)]
+@@ -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")]
+@@ -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
+diff --git a/lib/src/status.rs b/lib/src/status.rs
+index dba4889..e8f1fa5 100644
+--- a/lib/src/status.rs
++++ b/lib/src/status.rs
+@@ -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;
+@@ -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
+@@ -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();
+
+@@ -281,6 +293,7 @@ pub(crate) fn get_status(
+ staged,
+ booted,
+ rollback,
++ rollback_queued,
+ ty,
+ };
+ Ok((deployments, host))
+diff --git a/tests/integration/playbooks/rollback.yaml b/tests/integration/playbooks/rollback.yaml
+index a801656..e193ff5 100644
+--- a/tests/integration/playbooks/rollback.yaml
++++ b/tests/integration/playbooks/rollback.yaml
+@@ -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
+--
+2.43.0
+