Skip to content

Commit

Permalink
update_agent: support updating with OCI graph
Browse files Browse the repository at this point in the history
Local deployements are now unserialized with a enum being either
`Checksum` or `Pullspec`.
This will cue the cincinnati client to request the OCI graph in the
pullspec case. [1]

The custom-origin-description, if set, is passed along the rebase
call, and the custom-url is updated to match the new pullspec. [2]

Finally, update the polkit policy to allow Zincati
to do a rebase operation.

[1] Requires coreos/fedora-coreos-cincinnati#99
and coreos/rpm-ostree#5120
  • Loading branch information
jbtrystram committed Feb 7, 2025
1 parent ddc7085 commit 5b17cae
Show file tree
Hide file tree
Showing 10 changed files with 232 additions and 79 deletions.
3 changes: 2 additions & 1 deletion dist/polkit-1/rules.d/zincati.rules
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@
polkit.addRule(function(action, subject) {
if ((action.id == "org.projectatomic.rpmostree1.deploy" ||
action.id == "org.projectatomic.rpmostree1.finalize-deployment") ||
action.id == "org.projectatomic.rpmostree1.cleanup" &&
action.id == "org.projectatomic.rpmostree1.cleanup" ||
action.id == "org.projectatomic.rpmostree1.rebase" &&
subject.user == "zincati") {
return polkit.Result.YES;
}
Expand Down
59 changes: 39 additions & 20 deletions src/cincinnati/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ mod mock_tests;

use crate::config::inputs;
use crate::identity::Identity;
use crate::rpm_ostree::Release;
use crate::rpm_ostree::{Payload, Release};
use anyhow::{Context, Result};
use fn_error_context::context;
use futures::prelude::*;
Expand All @@ -33,7 +33,10 @@ pub static DEADEND_KEY: &str = "org.fedoraproject.coreos.updates.deadend";
pub static DEADEND_REASON_KEY: &str = "org.fedoraproject.coreos.updates.deadend_reason";

/// Metadata value for "checksum" payload scheme.
pub static CHECKSUM_SCHEME: &str = "checksum";
pub const CHECKSUM_SCHEME: &str = "checksum";

/// Metadata value for "oci" payload scheme.
pub const OCI_SCHEME: &str = "oci";

lazy_static::lazy_static! {
static ref GRAPH_NODES: IntGauge = register_int_gauge!(opts!(
Expand Down Expand Up @@ -229,7 +232,7 @@ fn find_update(
.nodes
.iter()
.enumerate()
.find(|(_, node)| is_same_checksum(node, &booted_depl.checksum))
.find(|(_, node)| is_same_checksum(node, &booted_depl))
{
Some(current) => current,
None => {
Expand Down Expand Up @@ -319,30 +322,42 @@ fn find_denylisted_releases(graph: &client::Graph, depls: BTreeSet<Release>) ->
use std::collections::HashSet;

let mut local_releases = BTreeSet::new();
let checksums: HashSet<String> = depls.into_iter().map(|rel| rel.checksum).collect();
let checksums: HashSet<Payload> = depls.into_iter().map(|rel| rel.payload).collect();

for entry in &graph.nodes {
if !checksums.contains(&entry.payload) {
continue;
}

if let Ok(release) = Release::from_cincinnati(entry.clone()) {
local_releases.insert(release);
if checksums.contains(&release.payload) {
local_releases.insert(release);
}
}
}

local_releases
}

/// Check whether input node matches current checksum.
fn is_same_checksum(node: &Node, checksum: &str) -> bool {
let payload_is_checksum = node
.metadata
.get(SCHEME_KEY)
.map(|v| v == CHECKSUM_SCHEME)
.unwrap_or(false);
fn is_same_checksum(node: &Node, deploy: &Release) -> bool {
let payload_type = node.metadata.get(SCHEME_KEY);

payload_is_checksum && node.payload == checksum
if let Some(scheme) = payload_type {
if scheme.as_str() == OCI_SCHEME {
if let Ok(Some(local_checksum)) = deploy.get_image_reference() {
local_checksum == node.payload
} else {
false
}
} else if scheme.as_str() == CHECKSUM_SCHEME {
if let Payload::Checksum(checksum) = &deploy.payload {
return &node.payload == checksum;
} else {
return false;
}
} else {
return false;
}
} else {
false
}
}

/// Check whether input node is a dead-end; if so, return the reason.
Expand Down Expand Up @@ -379,23 +394,27 @@ mod tests {

#[test]
fn source_node_comparison() {
let current = "current-sha";
let current = Release {
version: String::new(),
payload: Payload::Checksum("current-sha".to_string()),
age_index: None,
};

let mut metadata = HashMap::new();
metadata.insert(SCHEME_KEY.to_string(), CHECKSUM_SCHEME.to_string());
let matching = Node {
version: "v0".to_string(),
payload: current.to_string(),
payload: "current-sha".to_string(),
metadata,
};
assert!(is_same_checksum(&matching, current));
assert!(is_same_checksum(&matching, &current));

let mismatch = Node {
version: "v0".to_string(),
payload: "mismatch".to_string(),
metadata: HashMap::new(),
};
assert!(!is_same_checksum(&mismatch, current));
assert!(!is_same_checksum(&mismatch, &current));
}

#[test]
Expand Down
14 changes: 11 additions & 3 deletions src/identity/mod.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
mod platform;

use crate::config::inputs;
use crate::rpm_ostree;
use crate::{config::inputs, rpm_ostree::Payload};
use anyhow::{anyhow, ensure, Context, Result};
use fn_error_context::context;
use lazy_static::lazy_static;
Expand Down Expand Up @@ -133,12 +133,20 @@ impl Identity {
pub fn cincinnati_params(&self) -> HashMap<String, String> {
let mut vars = HashMap::new();
vars.insert("basearch".to_string(), self.basearch.clone());
vars.insert("os_checksum".to_string(), self.current_os.checksum.clone());
vars.insert("os_version".to_string(), self.current_os.version.clone());
vars.insert("group".to_string(), self.group.clone());
vars.insert("node_uuid".to_string(), self.node_uuid.lower_hex());
vars.insert("platform".to_string(), self.platform.clone());
vars.insert("stream".to_string(), self.stream.clone());
match &self.current_os.payload {
Payload::Checksum(checksum) => {
vars.insert("os_checksum".to_string(), checksum.clone());
}
Payload::Pullspec(image) => {
vars.insert("os_checksum".to_string(), image.clone());
vars.insert("oci".to_string(), "true".to_string());
}
}
if let Some(rw) = self.rollout_wariness {
vars.insert("rollout_wariness".to_string(), format!("{:.06}", rw));
}
Expand All @@ -151,7 +159,7 @@ impl Identity {
basearch: "mock-amd64".to_string(),
current_os: rpm_ostree::Release {
version: "0.0.0-mock".to_string(),
checksum: "sha-mock".to_string(),
payload: Payload::Checksum("sha-mock".to_string()),
age_index: None,
},
group: "mock-workers".to_string(),
Expand Down
54 changes: 51 additions & 3 deletions src/rpm_ostree/actor.rs
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
//! rpm-ostree client actor.
use super::cli_status::Status;
use super::Release;
use super::{Payload, Release};
use actix::prelude::*;
use anyhow::{Context, Result};
use filetime::FileTime;
use log::trace;
use ostree_ext::container::OstreeImageReference;
use std::collections::BTreeSet;
use std::rc::Rc;

Expand Down Expand Up @@ -52,8 +53,55 @@ impl Handler<StageDeployment> for RpmOstreeClient {
type Result = Result<Release>;

fn handle(&mut self, msg: StageDeployment, _ctx: &mut Self::Context) -> Self::Result {
trace!("request to stage release: {:?}", msg.release);
let release = super::cli_deploy::deploy_locked(msg.release, msg.allow_downgrade);
let booted = super::cli_status::invoke_cli_status(true)?;
let local_deploy = super::cli_status::booted_status(&booted)?;

let release = if let Payload::Pullspec(release_payload) = msg.release.payload {
if let Some(local_imgref) = local_deploy.container_image_reference() {
// re-use the custom origin info to the new deployment
// while updating custom-url to match the new image digest
// XXX: when moving to update graph V2 we want to remove this and
// have custom-url pointing to the OCI artifact containing the graph
// See https://github.com/coreos/fedora-coreos-tracker/issues/1872
let custom_origin = if let Some(mut custom_origin) = local_deploy.custom_origin() {
custom_origin.url = release_payload.clone();
Some(custom_origin)
} else {
log::warn!("Missing custom origin information for local OCI deployment.");
None
};

// Cinncinati payload contains the container image pullspec, but we need
// to prepend the OSTree signature source so rpm-ostree will verify the signature of
// the OSTree commit wrapped inside the container.

// let's craft a propper ostree imgref object
let rebase_target = OstreeImageReference {
sigverify: local_imgref.sigverify,
imgref: ostree_ext::container::ImageReference {
transport: ostree_ext::container::Transport::Registry,
name: release_payload,
},
};

// re-craft a release object with the pullspec
let oci_release = Release {
version: msg.release.version.clone(),
payload: Payload::Pullspec(rebase_target.to_string()),
age_index: msg.release.age_index,
};

trace!("request to stage release: {:?}", oci_release);
super::cli_deploy::deploy_locked(oci_release, msg.allow_downgrade, custom_origin)
} else {
// This should never happen as requesting the OCI graph only happens after we detected the local deployement is OCI.
// But let's fail gracefuly just in case.
anyhow::bail!("Zincati does not support OCI updates if the current deployement is not already an OCI image reference.")
}
} else {
trace!("request to stage release: {:?}", msg.release);
super::cli_deploy::deploy_locked(msg.release, msg.allow_downgrade, None)
};
trace!("rpm-ostree CLI returned: {:?}", release);
release
}
Expand Down
52 changes: 39 additions & 13 deletions src/rpm_ostree/cli_deploy.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
//! Interface to `rpm-ostree deploy --lock-finalization` and
//! `rpm-ostree deploy --register-driver`.
use super::Release;
use crate::rpm_ostree::{CustomOrigin, Release};
use anyhow::{bail, Context, Result};
use once_cell::sync::Lazy;
use prometheus::IntCounter;
Expand Down Expand Up @@ -32,10 +32,14 @@ static REGISTER_DRIVER_FAILURES: Lazy<IntCounter> = Lazy::new(|| {
});

/// Deploy an upgrade (by checksum) and leave the new deployment locked.
pub fn deploy_locked(release: Release, allow_downgrade: bool) -> Result<Release> {
pub fn deploy_locked(
release: Release,
allow_downgrade: bool,
custom_origin: Option<CustomOrigin>,
) -> Result<Release> {
DEPLOY_ATTEMPTS.inc();

let result = invoke_cli_deploy(release, allow_downgrade);
let result = invoke_cli_deploy(release, allow_downgrade, custom_origin);
if result.is_err() {
DEPLOY_FAILURES.inc();
}
Expand Down Expand Up @@ -94,16 +98,37 @@ fn invoke_cli_register() -> Result<()> {
}

/// CLI executor for deploying upgrades.
fn invoke_cli_deploy(release: Release, allow_downgrade: bool) -> Result<Release> {
fn invoke_cli_deploy(
release: Release,
allow_downgrade: bool,
custom_origin: Option<CustomOrigin>,
) -> Result<Release> {
fail_point!("deploy_locked_err", |_| bail!("deploy_locked_err"));
fail_point!("deploy_locked_ok", |_| Ok(release.clone()));

let mut cmd = std::process::Command::new("rpm-ostree");
cmd.arg("deploy")
.arg("--lock-finalization")
.arg("--skip-branch-check")
.arg(format!("revision={}", release.checksum))
.env("RPMOSTREE_CLIENT_ID", "zincati");
match &release.payload {
crate::rpm_ostree::Payload::Pullspec(image) => {
if let Some(origin) = custom_origin {
cmd.arg("rebase")
.arg(image)
.arg("--lock-finalization")
.arg("--custom-origin-url")
.arg(origin.url.clone())
.arg("--custom-origin-description")
.arg(origin.description.clone());
} else {
bail!("Missing custom-origin information.")
}
}
crate::rpm_ostree::Payload::Checksum(checksum) => {
cmd.arg("deploy")
.arg("--lock-finalization")
.arg("--skip-branch-check")
.arg(format!("revision={}", checksum));
}
}
cmd.env("RPMOSTREE_CLIENT_ID", "zincati");
if !allow_downgrade {
cmd.arg("--disallow-downgrade");
}
Expand Down Expand Up @@ -137,6 +162,7 @@ pub fn invoke_cli_cleanup() -> Result<()> {
mod tests {
#[allow(unused_imports)]
use super::*;
use crate::rpm_ostree::Payload;

#[cfg(feature = "failpoints")]
#[test]
Expand All @@ -146,10 +172,10 @@ mod tests {

let release = Release {
version: "foo".to_string(),
checksum: "bar".to_string(),
payload: Payload::Checksum("bar".to_string()),
age_index: None,
};
let result = deploy_locked(release, true);
let result = deploy_locked(release, true, None);
assert!(result.is_err());
assert!(DEPLOY_ATTEMPTS.get() >= 1);
assert!(DEPLOY_FAILURES.get() >= 1);
Expand All @@ -163,10 +189,10 @@ mod tests {

let release = Release {
version: "foo".to_string(),
checksum: "bar".to_string(),
payload: Payload::Checksum("bar".to_string()),
age_index: None,
};
let result = deploy_locked(release.clone(), true).unwrap();
let result = deploy_locked(release.clone(), true, None).unwrap();
assert_eq!(result, release);
assert!(DEPLOY_ATTEMPTS.get() >= 1);
}
Expand Down
24 changes: 16 additions & 8 deletions src/rpm_ostree/cli_finalize.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,18 +18,26 @@ lazy_static::lazy_static! {
/// Unlock and finalize the new deployment.
pub fn finalize_deployment(release: Release) -> Result<Release> {
FINALIZE_ATTEMPTS.inc();
let cmd = std::process::Command::new("rpm-ostree")
.arg("finalize-deployment")
.arg(&release.checksum)
.env("RPMOSTREE_CLIENT_ID", "zincati")
.output()
.context("failed to run 'rpm-ostree' binary")?;
let mut cmd = std::process::Command::new("rpm-ostree");
cmd.env("RPMOSTREE_CLIENT_ID", "zincati")
.arg("finalize-deployment");

if !cmd.status.success() {
// XXX for OCI image, we don't know the checksum until we deployed it.
// Currently, rpm-ostree do not return the resulting ostree commit
// when rebasing to an OCI image. We could query the deployements and
// get the latest commit but that would be racy, so let's finalize the latest
// commit.
match &release.payload {
super::Payload::Pullspec(_) => cmd.arg("--allow-missing-checksum"),
super::Payload::Checksum(checksum) => cmd.arg(checksum),
};

let cmd_result = cmd.output().context("failed to run 'rpm-ostree' binary")?;
if !cmd_result.status.success() {
FINALIZE_FAILURES.inc();
anyhow::bail!(
"rpm-ostree finalize-deployment failed:\n{}",
String::from_utf8_lossy(&cmd.stderr)
String::from_utf8_lossy(&cmd_result.stderr)
);
}

Expand Down
Loading

0 comments on commit 5b17cae

Please sign in to comment.